Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2012 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 // See http://www.softwareishard.com/blog/har-12-spec/
     32 // for HAR specification.
     33 
     34 // FIXME: Some fields are not yet supported due to back-end limitations.
     35 // See https://bugs.webkit.org/show_bug.cgi?id=58127 for details.
     36 
     37 /**
     38  * @constructor
     39  * @param {WebInspector.NetworkRequest} request
     40  */
     41 WebInspector.HAREntry = function(request)
     42 {
     43     this._request = request;
     44 }
     45 
     46 WebInspector.HAREntry.prototype = {
     47     /**
     48      * @return {Object}
     49      */
     50     build: function()
     51     {
     52         var entry =  {
     53             startedDateTime: new Date(this._request.startTime * 1000),
     54             time: WebInspector.HAREntry._toMilliseconds(this._request.duration),
     55             request: this._buildRequest(),
     56             response: this._buildResponse(),
     57             cache: { }, // Not supported yet.
     58             timings: this._buildTimings()
     59         };
     60         var page = WebInspector.networkLog.pageLoadForRequest(this._request);
     61         if (page)
     62             entry.pageref = "page_" + page.id;
     63         return entry;
     64     },
     65 
     66     /**
     67      * @return {Object}
     68      */
     69     _buildRequest: function()
     70     {
     71         var res = {
     72             method: this._request.requestMethod,
     73             url: this._buildRequestURL(this._request.url),
     74             httpVersion: this._request.requestHttpVersion,
     75             headers: this._request.requestHeaders,
     76             queryString: this._buildParameters(this._request.queryParameters || []),
     77             cookies: this._buildCookies(this._request.requestCookies || []),
     78             headersSize: this._request.requestHeadersSize,
     79             bodySize: this.requestBodySize
     80         };
     81         if (this._request.requestFormData)
     82             res.postData = this._buildPostData();
     83 
     84         return res;
     85     },
     86 
     87     /**
     88      * @return {Object}
     89      */
     90     _buildResponse: function()
     91     {
     92         return {
     93             status: this._request.statusCode,
     94             statusText: this._request.statusText,
     95             httpVersion: this._request.responseHttpVersion,
     96             headers: this._request.responseHeaders,
     97             cookies: this._buildCookies(this._request.responseCookies || []),
     98             content: this._buildContent(),
     99             redirectURL: this._request.responseHeaderValue("Location") || "",
    100             headersSize: this._request.responseHeadersSize,
    101             bodySize: this.responseBodySize
    102         };
    103     },
    104 
    105     /**
    106      * @return {Object}
    107      */
    108     _buildContent: function()
    109     {
    110         var content = {
    111             size: this._request.resourceSize,
    112             mimeType: this._request.mimeType,
    113             // text: this._request.content // TODO: pull out into a boolean flag, as content can be huge (and needs to be requested with an async call)
    114         };
    115         var compression = this.responseCompression;
    116         if (typeof compression === "number")
    117             content.compression = compression;
    118         return content;
    119     },
    120 
    121     /**
    122      * @return {Object}
    123      */
    124     _buildTimings: function()
    125     {
    126         var waitForConnection = this._interval("connectStart", "connectEnd");
    127         var blocked = 0;
    128         var connect = -1;
    129 
    130         if (this._request.connectionReused)
    131             blocked = waitForConnection;
    132         else
    133             connect = waitForConnection;
    134 
    135         return {
    136             blocked: blocked,
    137             dns: this._interval("dnsStart", "dnsEnd"),
    138             connect: connect,
    139             send: this._interval("sendStart", "sendEnd"),
    140             wait: this._interval("sendEnd", "receiveHeadersEnd"),
    141             receive: WebInspector.HAREntry._toMilliseconds(this._request.receiveDuration),
    142             ssl: this._interval("sslStart", "sslEnd")
    143         };
    144     },
    145 
    146     /**
    147      * @return {Object}
    148      */
    149     _buildPostData: function()
    150     {
    151         var res = {
    152             mimeType: this._request.requestHeaderValue("Content-Type"),
    153             text: this._request.requestFormData
    154         };
    155         if (this._request.formParameters)
    156             res.params = this._buildParameters(this._request.formParameters);
    157         return res;
    158     },
    159 
    160     /**
    161      * @param {Array.<Object>} parameters
    162      * @return {Array.<Object>}
    163      */
    164     _buildParameters: function(parameters)
    165     {
    166         return parameters.slice();
    167     },
    168 
    169     /**
    170      * @param {string} url
    171      * @return {string}
    172      */
    173     _buildRequestURL: function(url)
    174     {
    175         return url.split("#", 2)[0];
    176     },
    177 
    178     /**
    179      * @param {Array.<WebInspector.Cookie>} cookies
    180      * @return {Array.<Object>}
    181      */
    182     _buildCookies: function(cookies)
    183     {
    184         return cookies.map(this._buildCookie.bind(this));
    185     },
    186 
    187     /**
    188      * @param {WebInspector.Cookie} cookie
    189      * @return {Object}
    190      */
    191     _buildCookie: function(cookie)
    192     {
    193         return {
    194             name: cookie.name(),
    195             value: cookie.value(),
    196             path: cookie.path(),
    197             domain: cookie.domain(),
    198             expires: cookie.expiresDate(new Date(this._request.startTime * 1000)),
    199             httpOnly: cookie.httpOnly(),
    200             secure: cookie.secure()
    201         };
    202     },
    203 
    204     /**
    205      * @param {string} start
    206      * @param {string} end
    207      * @return {number}
    208      */
    209     _interval: function(start, end)
    210     {
    211         var timing = this._request.timing;
    212         if (!timing)
    213             return -1;
    214         var startTime = timing[start];
    215         return typeof startTime !== "number" || startTime === -1 ? -1 : Math.round(timing[end] - startTime);
    216     },
    217 
    218     /**
    219      * @return {number}
    220      */
    221     get requestBodySize()
    222     {
    223         return !this._request.requestFormData ? 0 : this._request.requestFormData.length;
    224     },
    225 
    226     /**
    227      * @return {number}
    228      */
    229     get responseBodySize()
    230     {
    231         if (this._request.cached || this._request.statusCode === 304)
    232             return 0;
    233         return this._request.transferSize - this._request.responseHeadersSize;
    234     },
    235 
    236     /**
    237      * @return {number|undefined}
    238      */
    239     get responseCompression()
    240     {
    241         if (this._request.cached || this._request.statusCode === 304 || this._request.statusCode === 206)
    242             return;
    243         return this._request.resourceSize - this.responseBodySize;
    244     }
    245 }
    246 
    247 /**
    248  * @param {number} time
    249  * @return {number}
    250  */
    251 WebInspector.HAREntry._toMilliseconds = function(time)
    252 {
    253     return time === -1 ? -1 : Math.round(time * 1000);
    254 }
    255 
    256 /**
    257  * @constructor
    258  * @param {Array.<WebInspector.NetworkRequest>} requests
    259  */
    260 WebInspector.HARLog = function(requests)
    261 {
    262     this._requests = requests;
    263 }
    264 
    265 WebInspector.HARLog.prototype = {
    266     /**
    267      * @return {Object}
    268      */
    269     build: function()
    270     {
    271         return {
    272             version: "1.2",
    273             creator: this._creator(),
    274             pages: this._buildPages(),
    275             entries: this._requests.map(this._convertResource.bind(this))
    276         }
    277     },
    278 
    279     _creator: function()
    280     {
    281         var webKitVersion = /AppleWebKit\/([^ ]+)/.exec(window.navigator.userAgent);
    282 
    283         return {
    284             name: "WebInspector",
    285             version: webKitVersion ? webKitVersion[1] : "n/a"
    286         };
    287     },
    288 
    289     /**
    290      * @return {Array}
    291      */
    292     _buildPages: function()
    293     {
    294         var seenIdentifiers = {};
    295         var pages = [];
    296         for (var i = 0; i < this._requests.length; ++i) {
    297             var page = WebInspector.networkLog.pageLoadForRequest(this._requests[i]);
    298             if (!page || seenIdentifiers[page.id])
    299                 continue;
    300             seenIdentifiers[page.id] = true;
    301             pages.push(this._convertPage(page));
    302         }
    303         return pages;
    304     },
    305 
    306     /**
    307      * @param {WebInspector.PageLoad} page
    308      * @return {Object}
    309      */
    310     _convertPage: function(page)
    311     {
    312         return {
    313             startedDateTime: new Date(page.startTime * 1000),
    314             id: "page_" + page.id,
    315             title: page.url, // We don't have actual page title here. URL is probably better than nothing.
    316             pageTimings: {
    317                 onContentLoad: this._pageEventTime(page, page.contentLoadTime),
    318                 onLoad: this._pageEventTime(page, page.loadTime)
    319             }
    320         }
    321     },
    322 
    323     /**
    324      * @param {WebInspector.NetworkRequest} request
    325      * @return {Object}
    326      */
    327     _convertResource: function(request)
    328     {
    329         return (new WebInspector.HAREntry(request)).build();
    330     },
    331 
    332     /**
    333      * @param {WebInspector.PageLoad} page
    334      * @param {number} time
    335      * @return {number}
    336      */
    337     _pageEventTime: function(page, time)
    338     {
    339         var startTime = page.startTime;
    340         if (time === -1 || startTime === -1)
    341             return -1;
    342         return WebInspector.HAREntry._toMilliseconds(time - startTime);
    343     }
    344 }
    345 
    346 /**
    347  * @constructor
    348  */
    349 WebInspector.HARWriter = function()
    350 {
    351 }
    352 
    353 WebInspector.HARWriter.prototype = {
    354     /**
    355      * @param {WebInspector.OutputStream} stream
    356      * @param {Array.<WebInspector.NetworkRequest>} requests
    357      * @param {WebInspector.Progress} progress
    358      */
    359     write: function(stream, requests, progress)
    360     {
    361         this._stream = stream;
    362         this._harLog = (new WebInspector.HARLog(requests)).build();
    363         this._pendingRequests = 1; // Guard against completing resource transfer before all requests are made.
    364         var entries = this._harLog.entries;
    365         for (var i = 0; i < entries.length; ++i) {
    366             var content = requests[i].content;
    367             if (typeof content === "undefined" && requests[i].finished) {
    368                 ++this._pendingRequests;
    369                 requests[i].requestContent(this._onContentAvailable.bind(this, entries[i]));
    370             } else if (content !== null)
    371                 entries[i].response.content.text = content;
    372         }
    373         var compositeProgress = new WebInspector.CompositeProgress(progress);
    374         this._writeProgress = compositeProgress.createSubProgress();
    375         if (--this._pendingRequests) {
    376             this._requestsProgress = compositeProgress.createSubProgress();
    377             this._requestsProgress.setTitle(WebInspector.UIString("Collecting content"));
    378             this._requestsProgress.setTotalWork(this._pendingRequests);
    379         } else
    380             this._beginWrite();
    381     },
    382 
    383     /**
    384      * @param {Object} entry
    385      * @param {string|null} content
    386      * @param {boolean} contentEncoded
    387      * @param {string=} mimeType
    388      */
    389     _onContentAvailable: function(entry, content, contentEncoded, mimeType)
    390     {
    391         if (content !== null)
    392             entry.response.content.text = content;
    393         if (this._requestsProgress)
    394             this._requestsProgress.worked();
    395         if (!--this._pendingRequests) {
    396             this._requestsProgress.done();
    397             this._beginWrite();
    398         }
    399     },
    400 
    401     _beginWrite: function()
    402     {
    403         const jsonIndent = 2;
    404         this._text = JSON.stringify({log: this._harLog}, null, jsonIndent);
    405         this._writeProgress.setTitle(WebInspector.UIString("Writing file"));
    406         this._writeProgress.setTotalWork(this._text.length);
    407         this._bytesWritten = 0;
    408         this._writeNextChunk(this._stream);
    409     },
    410 
    411     /**
    412      * @param {WebInspector.OutputStream} stream
    413      * @param {string=} error
    414      */
    415     _writeNextChunk: function(stream, error)
    416     {
    417         if (this._bytesWritten >= this._text.length || error) {
    418             stream.close();
    419             this._writeProgress.done();
    420             return;
    421         }
    422         const chunkSize = 100000;
    423         var text = this._text.substring(this._bytesWritten, this._bytesWritten + chunkSize);
    424         this._bytesWritten += text.length;
    425         stream.write(text, this._writeNextChunk.bind(this));
    426         this._writeProgress.setWorked(this._bytesWritten);
    427     }
    428 }
    429