2 * Copyright (C) 2012 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
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
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.
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.
31 // See http://www.softwareishard.com/blog/har-12-spec/
32 // for HAR specification.
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.
39 * @param {!WebInspector.NetworkRequest} request
41 WebInspector.HAREntry = function(request)
43 this._request = request;
46 WebInspector.HAREntry.prototype = {
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()
61 if (this._request.connectionId)
62 entry.connection = String(this._request.connectionId);
63 var page = WebInspector.networkLog.pageLoadForRequest(this._request);
65 entry.pageref = "page_" + page.id;
72 _buildRequest: function()
74 var headersText = this._request.requestHeadersText();
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
85 if (this._request.requestFormData)
86 res.postData = this._buildPostData();
94 _buildResponse: function()
96 var headersText = this._request.responseHeadersText;
98 status: this._request.statusCode,
99 statusText: this._request.statusText,
100 httpVersion: this._request.responseHttpVersion,
101 headers: this._request.responseHeaders,
102 cookies: this._buildCookies(this._request.responseCookies || []),
103 content: this._buildContent(),
104 redirectURL: this._request.responseHeaderValue("Location") || "",
105 headersSize: headersText ? headersText.length : -1,
106 bodySize: this.responseBodySize,
107 _error: this._request.localizedFailDescription
114 _buildContent: function()
117 size: this._request.resourceSize,
118 mimeType: this._request.mimeType || "x-unknown",
119 // 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)
121 var compression = this.responseCompression;
122 if (typeof compression === "number")
123 content.compression = compression;
130 _buildTimings: function()
132 // Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], receive_headers_end
133 // HAR 'blocked' time is time before first network activity.
135 var timing = this._request.timing;
137 return {blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive: 0, ssl: -1};
139 function firstNonNegative(values)
141 for (var i = 0; i < values.length; ++i) {
145 console.assert(false, "Incomplete requet timing information.");
148 var blocked = firstNonNegative([timing.dnsStart, timing.connectStart, timing.sendStart]);
151 if (timing.dnsStart >= 0)
152 dns = firstNonNegative([timing.connectStart, timing.sendStart]) - timing.dnsStart;
155 if (timing.connectStart >= 0)
156 connect = timing.sendStart - timing.connectStart;
158 var send = timing.sendEnd - timing.sendStart;
159 var wait = timing.receiveHeadersEnd - timing.sendEnd;
160 var receive = WebInspector.HAREntry._toMilliseconds(this._request.duration) - timing.receiveHeadersEnd;
163 if (timing.sslStart >= 0 && timing.sslEnd >= 0)
164 ssl = timing.sslEnd - timing.sslStart;
166 return {blocked: blocked, dns: dns, connect: connect, send: send, wait: wait, receive: receive, ssl: ssl};
172 _buildPostData: function()
175 mimeType: this._request.requestContentType(),
176 text: this._request.requestFormData
178 if (this._request.formParameters)
179 res.params = this._buildParameters(this._request.formParameters);
184 * @param {!Array.<!Object>} parameters
185 * @return {!Array.<!Object>}
187 _buildParameters: function(parameters)
189 return parameters.slice();
193 * @param {string} url
196 _buildRequestURL: function(url)
198 return url.split("#", 2)[0];
202 * @param {!Array.<!WebInspector.Cookie>} cookies
203 * @return {!Array.<!Object>}
205 _buildCookies: function(cookies)
207 return cookies.map(this._buildCookie.bind(this));
211 * @param {!WebInspector.Cookie} cookie
214 _buildCookie: function(cookie)
218 value: cookie.value(),
220 domain: cookie.domain(),
221 expires: cookie.expiresDate(new Date(this._request.startTime * 1000)),
222 httpOnly: cookie.httpOnly(),
223 secure: cookie.secure()
230 get requestBodySize()
232 return !this._request.requestFormData ? 0 : this._request.requestFormData.length;
238 get responseBodySize()
240 if (this._request.cached || this._request.statusCode === 304)
242 if (!this._request.responseHeadersText)
244 return this._request.transferSize - this._request.responseHeadersText.length;
248 * @return {number|undefined}
250 get responseCompression()
252 if (this._request.cached || this._request.statusCode === 304 || this._request.statusCode === 206)
254 if (!this._request.responseHeadersText)
256 return this._request.resourceSize - this.responseBodySize;
261 * @param {number} time
264 WebInspector.HAREntry._toMilliseconds = function(time)
266 return time === -1 ? -1 : time * 1000;
271 * @param {!Array.<!WebInspector.NetworkRequest>} requests
273 WebInspector.HARLog = function(requests)
275 this._requests = requests;
278 WebInspector.HARLog.prototype = {
286 creator: this._creator(),
287 pages: this._buildPages(),
288 entries: this._requests.map(this._convertResource.bind(this))
294 var webKitVersion = /AppleWebKit\/([^ ]+)/.exec(window.navigator.userAgent);
297 name: "WebInspector",
298 version: webKitVersion ? webKitVersion[1] : "n/a"
303 * @return {!Array.<!Object>}
305 _buildPages: function()
307 var seenIdentifiers = {};
309 for (var i = 0; i < this._requests.length; ++i) {
310 var page = WebInspector.networkLog.pageLoadForRequest(this._requests[i]);
311 if (!page || seenIdentifiers[page.id])
313 seenIdentifiers[page.id] = true;
314 pages.push(this._convertPage(page));
320 * @param {!WebInspector.PageLoad} page
323 _convertPage: function(page)
326 startedDateTime: new Date(page.startTime * 1000),
327 id: "page_" + page.id,
328 title: page.url, // We don't have actual page title here. URL is probably better than nothing.
330 onContentLoad: this._pageEventTime(page, page.contentLoadTime),
331 onLoad: this._pageEventTime(page, page.loadTime)
337 * @param {!WebInspector.NetworkRequest} request
340 _convertResource: function(request)
342 return (new WebInspector.HAREntry(request)).build();
346 * @param {!WebInspector.PageLoad} page
347 * @param {number} time
350 _pageEventTime: function(page, time)
352 var startTime = page.startTime;
353 if (time === -1 || startTime === -1)
355 return WebInspector.HAREntry._toMilliseconds(time - startTime);
362 WebInspector.HARWriter = function()
366 WebInspector.HARWriter.prototype = {
368 * @param {!WebInspector.OutputStream} stream
369 * @param {!Array.<!WebInspector.NetworkRequest>} requests
370 * @param {!WebInspector.Progress} progress
372 write: function(stream, requests, progress)
374 this._stream = stream;
375 this._harLog = (new WebInspector.HARLog(requests)).build();
376 this._pendingRequests = 1; // Guard against completing resource transfer before all requests are made.
377 var entries = this._harLog.entries;
378 for (var i = 0; i < entries.length; ++i) {
379 var content = requests[i].content;
380 if (typeof content === "undefined" && requests[i].finished) {
381 ++this._pendingRequests;
382 requests[i].requestContent(this._onContentAvailable.bind(this, entries[i]));
383 } else if (content !== null)
384 entries[i].response.content.text = content;
386 var compositeProgress = new WebInspector.CompositeProgress(progress);
387 this._writeProgress = compositeProgress.createSubProgress();
388 if (--this._pendingRequests) {
389 this._requestsProgress = compositeProgress.createSubProgress();
390 this._requestsProgress.setTitle(WebInspector.UIString("Collecting content…"));
391 this._requestsProgress.setTotalWork(this._pendingRequests);
397 * @param {!Object} entry
398 * @param {?string} content
400 _onContentAvailable: function(entry, content)
402 if (content !== null)
403 entry.response.content.text = content;
404 if (this._requestsProgress)
405 this._requestsProgress.worked();
406 if (!--this._pendingRequests) {
407 this._requestsProgress.done();
412 _beginWrite: function()
414 const jsonIndent = 2;
415 this._text = JSON.stringify({log: this._harLog}, null, jsonIndent);
416 this._writeProgress.setTitle(WebInspector.UIString("Writing file…"));
417 this._writeProgress.setTotalWork(this._text.length);
418 this._bytesWritten = 0;
419 this._writeNextChunk(this._stream);
423 * @param {!WebInspector.OutputStream} stream
424 * @param {string=} error
426 _writeNextChunk: function(stream, error)
428 if (this._bytesWritten >= this._text.length || error) {
430 this._writeProgress.done();
433 const chunkSize = 100000;
434 var text = this._text.substring(this._bytesWritten, this._bytesWritten + chunkSize);
435 this._bytesWritten += text.length;
436 stream.write(text, this._writeNextChunk.bind(this));
437 this._writeProgress.setWorked(this._bytesWritten);