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: this._request.timing ? WebInspector.HAREntry._toMilliseconds(this._request.duration) : 0,
     55             request: this._buildRequest(),
     56             response: this._buildResponse(),
     57             cache: { }, // Not supported yet.
     58             timings: this._buildTimings()
     59         };
     60 
     61         if (this._request.connectionId)
     62             entry.connection = String(this._request.connectionId);
     63         var page = WebInspector.networkLog.pageLoadForRequest(this._request);
     64         if (page)
     65             entry.pageref = "page_" + page.id;
     66         return entry;
     67     },
     68 
     69     /**
     70      * @return {!Object}
     71      */
     72     _buildRequest: function()
     73     {
     74         var headersText = this._request.requestHeadersText();
     75         var res = {
     76             method: this._request.requestMethod,
     77             url: this._buildRequestURL(this._request.url),
     78             httpVersion: this._request.requestHttpVersion(),
     79             headers: this._request.requestHeaders(),
     80             queryString: this._buildParameters(this._request.queryParameters || []),
     81             cookies: this._buildCookies(this._request.requestCookies || []),
     82             headersSize: headersText ? headersText.length : -1,
     83             bodySize: this.requestBodySize
     84         };
     85         if (this._request.requestFormData)
     86             res.postData = this._buildPostData();
     87 
     88         return res;
     89     },
     90 
     91     /**
     92      * @return {!Object}
     93      */
     94     _buildResponse: function()
     95     {
     96         return {
     97             status: this._request.statusCode,
     98             statusText: this._request.statusText,
     99             httpVersion: this._request.responseHttpVersion,
    100             headers: this._request.responseHeaders,
    101             cookies: this._buildCookies(this._request.responseCookies || []),
    102             content: this._buildContent(),
    103             redirectURL: this._request.responseHeaderValue("Location") || "",
    104             headersSize: this._request.responseHeadersSize,
    105             bodySize: this.responseBodySize
    106         };
    107     },
    108 
    109     /**
    110      * @return {!Object}
    111      */
    112     _buildContent: function()
    113     {
    114         var content = {
    115             size: this._request.resourceSize,
    116             mimeType: this._request.mimeType,
    117             // 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)
    118         };
    119         var compression = this.responseCompression;
    120         if (typeof compression === "number")
    121             content.compression = compression;
    122         return content;
    123     },
    124 
    125     /**
    126      * @return {!Object}
    127      */
    128     _buildTimings: function()
    129     {
    130         // Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], receive_headers_end
    131         // HAR 'blocked' time is time before first network activity.
    132 
    133         var timing = this._request.timing;
    134         if (!timing)
    135             return {blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive: 0, ssl: -1};
    136 
    137         function firstNonNegative(values)
    138         {
    139             for (var i = 0; i < values.length; ++i) {
    140                 if (values[i] >= 0)
    141                     return values[i];
    142             }
    143             console.assert(false, "Incomplete requet timing information.");
    144         }
    145 
    146         var blocked = firstNonNegative([timing.dnsStart, timing.connectStart, timing.sendStart]);
    147 
    148         var dns = -1;
    149         if (timing.dnsStart >= 0)
    150             dns = firstNonNegative([timing.connectStart, timing.sendStart]) - timing.dnsStart;
    151 
    152         var connect = -1;
    153         if (timing.connectStart >= 0)
    154             connect = timing.sendStart - timing.connectStart;
    155 
    156         var send = timing.sendEnd - timing.sendStart;
    157         var wait = timing.receiveHeadersEnd - timing.sendEnd;
    158         var receive = WebInspector.HAREntry._toMilliseconds(this._request.duration) - timing.receiveHeadersEnd;
    159 
    160         var ssl = -1;
    161         if (timing.sslStart >= 0 && timing.sslEnd >= 0)
    162             ssl = timing.sslEnd - timing.sslStart;
    163 
    164         return {blocked: blocked, dns: dns, connect: connect, send: send, wait: wait, receive: receive, ssl: ssl};
    165     },
    166 
    167     /**
    168      * @return {!Object}
    169      */
    170     _buildPostData: function()
    171     {
    172         var res = {
    173             mimeType: this._request.requestContentType(),
    174             text: this._request.requestFormData
    175         };
    176         if (this._request.formParameters)
    177             res.params = this._buildParameters(this._request.formParameters);
    178         return res;
    179     },
    180 
    181     /**
    182      * @param {!Array.<!Object>} parameters
    183      * @return {!Array.<!Object>}
    184      */
    185     _buildParameters: function(parameters)
    186     {
    187         return parameters.slice();
    188     },
    189 
    190     /**
    191      * @param {string} url
    192      * @return {string}
    193      */
    194     _buildRequestURL: function(url)
    195     {
    196         return url.split("#", 2)[0];
    197     },
    198 
    199     /**
    200      * @param {!Array.<!WebInspector.Cookie>} cookies
    201      * @return {!Array.<!Object>}
    202      */
    203     _buildCookies: function(cookies)
    204     {
    205         return cookies.map(this._buildCookie.bind(this));
    206     },
    207 
    208     /**
    209      * @param {!WebInspector.Cookie} cookie
    210      * @return {!Object}
    211      */
    212     _buildCookie: function(cookie)
    213     {
    214         return {
    215             name: cookie.name(),
    216             value: cookie.value(),
    217             path: cookie.path(),
    218             domain: cookie.domain(),
    219             expires: cookie.expiresDate(new Date(this._request.startTime * 1000)),
    220             httpOnly: cookie.httpOnly(),
    221             secure: cookie.secure()
    222         };
    223     },
    224 
    225     /**
    226      * @return {number}
    227      */
    228     get requestBodySize()
    229     {
    230         return !this._request.requestFormData ? 0 : this._request.requestFormData.length;
    231     },
    232 
    233     /**
    234      * @return {number}
    235      */
    236     get responseBodySize()
    237     {
    238         if (this._request.cached || this._request.statusCode === 304)
    239             return 0;
    240         return this._request.transferSize - this._request.responseHeadersSize;
    241     },
    242 
    243     /**
    244      * @return {number|undefined}
    245      */
    246     get responseCompression()
    247     {
    248         if (this._request.cached || this._request.statusCode === 304 || this._request.statusCode === 206)
    249             return;
    250         return this._request.resourceSize - this.responseBodySize;
    251     }
    252 }
    253 
    254 /**
    255  * @param {number} time
    256  * @return {number}
    257  */
    258 WebInspector.HAREntry._toMilliseconds = function(time)
    259 {
    260     return time === -1 ? -1 : time * 1000;
    261 }
    262 
    263 /**
    264  * @constructor
    265  * @param {!Array.<!WebInspector.NetworkRequest>} requests
    266  */
    267 WebInspector.HARLog = function(requests)
    268 {
    269     this._requests = requests;
    270 }
    271 
    272 WebInspector.HARLog.prototype = {
    273     /**
    274      * @return {!Object}
    275      */
    276     build: function()
    277     {
    278         return {
    279             version: "1.2",
    280             creator: this._creator(),
    281             pages: this._buildPages(),
    282             entries: this._requests.map(this._convertResource.bind(this))
    283         }
    284     },
    285 
    286     _creator: function()
    287     {
    288         var webKitVersion = /AppleWebKit\/([^ ]+)/.exec(window.navigator.userAgent);
    289 
    290         return {
    291             name: "WebInspector",
    292             version: webKitVersion ? webKitVersion[1] : "n/a"
    293         };
    294     },
    295 
    296     /**
    297      * @return {!Array.<!Object>}
    298      */
    299     _buildPages: function()
    300     {
    301         var seenIdentifiers = {};
    302         var pages = [];
    303         for (var i = 0; i < this._requests.length; ++i) {
    304             var page = WebInspector.networkLog.pageLoadForRequest(this._requests[i]);
    305             if (!page || seenIdentifiers[page.id])
    306                 continue;
    307             seenIdentifiers[page.id] = true;
    308             pages.push(this._convertPage(page));
    309         }
    310         return pages;
    311     },
    312 
    313     /**
    314      * @param {!WebInspector.PageLoad} page
    315      * @return {!Object}
    316      */
    317     _convertPage: function(page)
    318     {
    319         return {
    320             startedDateTime: new Date(page.startTime * 1000),
    321             id: "page_" + page.id,
    322             title: page.url, // We don't have actual page title here. URL is probably better than nothing.
    323             pageTimings: {
    324                 onContentLoad: this._pageEventTime(page, page.contentLoadTime),
    325                 onLoad: this._pageEventTime(page, page.loadTime)
    326             }
    327         }
    328     },
    329 
    330     /**
    331      * @param {!WebInspector.NetworkRequest} request
    332      * @return {!Object}
    333      */
    334     _convertResource: function(request)
    335     {
    336         return (new WebInspector.HAREntry(request)).build();
    337     },
    338 
    339     /**
    340      * @param {!WebInspector.PageLoad} page
    341      * @param {number} time
    342      * @return {number}
    343      */
    344     _pageEventTime: function(page, time)
    345     {
    346         var startTime = page.startTime;
    347         if (time === -1 || startTime === -1)
    348             return -1;
    349         return WebInspector.HAREntry._toMilliseconds(time - startTime);
    350     }
    351 }
    352 
    353 /**
    354  * @constructor
    355  */
    356 WebInspector.HARWriter = function()
    357 {
    358 }
    359 
    360 WebInspector.HARWriter.prototype = {
    361     /**
    362      * @param {!WebInspector.OutputStream} stream
    363      * @param {!Array.<!WebInspector.NetworkRequest>} requests
    364      * @param {!WebInspector.Progress} progress
    365      */
    366     write: function(stream, requests, progress)
    367     {
    368         this._stream = stream;
    369         this._harLog = (new WebInspector.HARLog(requests)).build();
    370         this._pendingRequests = 1; // Guard against completing resource transfer before all requests are made.
    371         var entries = this._harLog.entries;
    372         for (var i = 0; i < entries.length; ++i) {
    373             var content = requests[i].content;
    374             if (typeof content === "undefined" && requests[i].finished) {
    375                 ++this._pendingRequests;
    376                 requests[i].requestContent(this._onContentAvailable.bind(this, entries[i]));
    377             } else if (content !== null)
    378                 entries[i].response.content.text = content;
    379         }
    380         var compositeProgress = new WebInspector.CompositeProgress(progress);
    381         this._writeProgress = compositeProgress.createSubProgress();
    382         if (--this._pendingRequests) {
    383             this._requestsProgress = compositeProgress.createSubProgress();
    384             this._requestsProgress.setTitle(WebInspector.UIString("Collecting content"));
    385             this._requestsProgress.setTotalWork(this._pendingRequests);
    386         } else
    387             this._beginWrite();
    388     },
    389 
    390     /**
    391      * @param {!Object} entry
    392      * @param {?string} content
    393      */
    394     _onContentAvailable: function(entry, content)
    395     {
    396         if (content !== null)
    397             entry.response.content.text = content;
    398         if (this._requestsProgress)
    399             this._requestsProgress.worked();
    400         if (!--this._pendingRequests) {
    401             this._requestsProgress.done();
    402             this._beginWrite();
    403         }
    404     },
    405 
    406     _beginWrite: function()
    407     {
    408         const jsonIndent = 2;
    409         this._text = JSON.stringify({log: this._harLog}, null, jsonIndent);
    410         this._writeProgress.setTitle(WebInspector.UIString("Writing file"));
    411         this._writeProgress.setTotalWork(this._text.length);
    412         this._bytesWritten = 0;
    413         this._writeNextChunk(this._stream);
    414     },
    415 
    416     /**
    417      * @param {!WebInspector.OutputStream} stream
    418      * @param {string=} error
    419      */
    420     _writeNextChunk: function(stream, error)
    421     {
    422         if (this._bytesWritten >= this._text.length || error) {
    423             stream.close();
    424             this._writeProgress.done();
    425             return;
    426         }
    427         const chunkSize = 100000;
    428         var text = this._text.substring(this._bytesWritten, this._bytesWritten + chunkSize);
    429         this._bytesWritten += text.length;
    430         stream.write(text, this._writeNextChunk.bind(this));
    431         this._writeProgress.setWorked(this._bytesWritten);
    432     }
    433 }
    434