OLD | NEW |
| (Empty) |
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(t
his._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 !== "0") | |
62 entry.connection = this._request.connectionId; | |
63 var page = this._request.target().networkLog.pageLoadForRequest(this._re
quest); | |
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 var headersText = this._request.responseHeadersText; | |
97 return { | |
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 | |
108 }; | |
109 }, | |
110 | |
111 /** | |
112 * @return {!Object} | |
113 */ | |
114 _buildContent: function() | |
115 { | |
116 var content = { | |
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) | |
120 }; | |
121 var compression = this.responseCompression; | |
122 if (typeof compression === "number") | |
123 content.compression = compression; | |
124 return content; | |
125 }, | |
126 | |
127 /** | |
128 * @return {!Object} | |
129 */ | |
130 _buildTimings: function() | |
131 { | |
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. | |
134 | |
135 var timing = this._request.timing; | |
136 if (!timing) | |
137 return {blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive
: 0, ssl: -1}; | |
138 | |
139 function firstNonNegative(values) | |
140 { | |
141 for (var i = 0; i < values.length; ++i) { | |
142 if (values[i] >= 0) | |
143 return values[i]; | |
144 } | |
145 console.assert(false, "Incomplete requet timing information."); | |
146 } | |
147 | |
148 var blocked = firstNonNegative([timing.dnsStart, timing.connectStart, ti
ming.sendStart]); | |
149 | |
150 var dns = -1; | |
151 if (timing.dnsStart >= 0) | |
152 dns = firstNonNegative([timing.connectStart, timing.sendStart]) - ti
ming.dnsStart; | |
153 | |
154 var connect = -1; | |
155 if (timing.connectStart >= 0) | |
156 connect = timing.sendStart - timing.connectStart; | |
157 | |
158 var send = timing.sendEnd - timing.sendStart; | |
159 var wait = timing.receiveHeadersEnd - timing.sendEnd; | |
160 var receive = WebInspector.HAREntry._toMilliseconds(this._request.durati
on) - timing.receiveHeadersEnd; | |
161 | |
162 var ssl = -1; | |
163 if (timing.sslStart >= 0 && timing.sslEnd >= 0) | |
164 ssl = timing.sslEnd - timing.sslStart; | |
165 | |
166 return {blocked: blocked, dns: dns, connect: connect, send: send, wait:
wait, receive: receive, ssl: ssl}; | |
167 }, | |
168 | |
169 /** | |
170 * @return {!Object} | |
171 */ | |
172 _buildPostData: function() | |
173 { | |
174 var res = { | |
175 mimeType: this._request.requestContentType(), | |
176 text: this._request.requestFormData | |
177 }; | |
178 if (this._request.formParameters) | |
179 res.params = this._buildParameters(this._request.formParameters); | |
180 return res; | |
181 }, | |
182 | |
183 /** | |
184 * @param {!Array.<!Object>} parameters | |
185 * @return {!Array.<!Object>} | |
186 */ | |
187 _buildParameters: function(parameters) | |
188 { | |
189 return parameters.slice(); | |
190 }, | |
191 | |
192 /** | |
193 * @param {string} url | |
194 * @return {string} | |
195 */ | |
196 _buildRequestURL: function(url) | |
197 { | |
198 return url.split("#", 2)[0]; | |
199 }, | |
200 | |
201 /** | |
202 * @param {!Array.<!WebInspector.Cookie>} cookies | |
203 * @return {!Array.<!Object>} | |
204 */ | |
205 _buildCookies: function(cookies) | |
206 { | |
207 return cookies.map(this._buildCookie.bind(this)); | |
208 }, | |
209 | |
210 /** | |
211 * @param {!WebInspector.Cookie} cookie | |
212 * @return {!Object} | |
213 */ | |
214 _buildCookie: function(cookie) | |
215 { | |
216 return { | |
217 name: cookie.name(), | |
218 value: cookie.value(), | |
219 path: cookie.path(), | |
220 domain: cookie.domain(), | |
221 expires: cookie.expiresDate(new Date(this._request.startTime * 1000)
), | |
222 httpOnly: cookie.httpOnly(), | |
223 secure: cookie.secure() | |
224 }; | |
225 }, | |
226 | |
227 /** | |
228 * @return {number} | |
229 */ | |
230 get requestBodySize() | |
231 { | |
232 return !this._request.requestFormData ? 0 : this._request.requestFormDat
a.length; | |
233 }, | |
234 | |
235 /** | |
236 * @return {number} | |
237 */ | |
238 get responseBodySize() | |
239 { | |
240 if (this._request.cached() || this._request.statusCode === 304) | |
241 return 0; | |
242 if (!this._request.responseHeadersText) | |
243 return -1; | |
244 return this._request.transferSize - this._request.responseHeadersText.le
ngth; | |
245 }, | |
246 | |
247 /** | |
248 * @return {number|undefined} | |
249 */ | |
250 get responseCompression() | |
251 { | |
252 if (this._request.cached() || this._request.statusCode === 304 || this._
request.statusCode === 206) | |
253 return; | |
254 if (!this._request.responseHeadersText) | |
255 return; | |
256 return this._request.resourceSize - this.responseBodySize; | |
257 } | |
258 } | |
259 | |
260 /** | |
261 * @param {number} time | |
262 * @return {number} | |
263 */ | |
264 WebInspector.HAREntry._toMilliseconds = function(time) | |
265 { | |
266 return time === -1 ? -1 : time * 1000; | |
267 } | |
268 | |
269 /** | |
270 * @constructor | |
271 * @param {!Array.<!WebInspector.NetworkRequest>} requests | |
272 */ | |
273 WebInspector.HARLog = function(requests) | |
274 { | |
275 this._requests = requests; | |
276 } | |
277 | |
278 WebInspector.HARLog.prototype = { | |
279 /** | |
280 * @return {!Object} | |
281 */ | |
282 build: function() | |
283 { | |
284 return { | |
285 version: "1.2", | |
286 creator: this._creator(), | |
287 pages: this._buildPages(), | |
288 entries: this._requests.map(this._convertResource.bind(this)) | |
289 } | |
290 }, | |
291 | |
292 _creator: function() | |
293 { | |
294 var webKitVersion = /AppleWebKit\/([^ ]+)/.exec(window.navigator.userAge
nt); | |
295 | |
296 return { | |
297 name: "WebInspector", | |
298 version: webKitVersion ? webKitVersion[1] : "n/a" | |
299 }; | |
300 }, | |
301 | |
302 /** | |
303 * @return {!Array.<!Object>} | |
304 */ | |
305 _buildPages: function() | |
306 { | |
307 var seenIdentifiers = {}; | |
308 var pages = []; | |
309 for (var i = 0; i < this._requests.length; ++i) { | |
310 var page = this._requests[i].target().networkLog.pageLoadForRequest(
this._requests[i]); | |
311 if (!page || seenIdentifiers[page.id]) | |
312 continue; | |
313 seenIdentifiers[page.id] = true; | |
314 pages.push(this._convertPage(page)); | |
315 } | |
316 return pages; | |
317 }, | |
318 | |
319 /** | |
320 * @param {!WebInspector.PageLoad} page | |
321 * @return {!Object} | |
322 */ | |
323 _convertPage: function(page) | |
324 { | |
325 return { | |
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 pro
bably better than nothing. | |
329 pageTimings: { | |
330 onContentLoad: this._pageEventTime(page, page.contentLoadTime), | |
331 onLoad: this._pageEventTime(page, page.loadTime) | |
332 } | |
333 } | |
334 }, | |
335 | |
336 /** | |
337 * @param {!WebInspector.NetworkRequest} request | |
338 * @return {!Object} | |
339 */ | |
340 _convertResource: function(request) | |
341 { | |
342 return (new WebInspector.HAREntry(request)).build(); | |
343 }, | |
344 | |
345 /** | |
346 * @param {!WebInspector.PageLoad} page | |
347 * @param {number} time | |
348 * @return {number} | |
349 */ | |
350 _pageEventTime: function(page, time) | |
351 { | |
352 var startTime = page.startTime; | |
353 if (time === -1 || startTime === -1) | |
354 return -1; | |
355 return WebInspector.HAREntry._toMilliseconds(time - startTime); | |
356 } | |
357 } | |
358 | |
359 /** | |
360 * @constructor | |
361 */ | |
362 WebInspector.HARWriter = function() | |
363 { | |
364 } | |
365 | |
366 WebInspector.HARWriter.prototype = { | |
367 /** | |
368 * @param {!WebInspector.OutputStream} stream | |
369 * @param {!Array.<!WebInspector.NetworkRequest>} requests | |
370 * @param {!WebInspector.Progress} progress | |
371 */ | |
372 write: function(stream, requests, progress) | |
373 { | |
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, e
ntries[i])); | |
383 } else if (content !== null) | |
384 entries[i].response.content.text = content; | |
385 } | |
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 co
ntent…")); | |
391 this._requestsProgress.setTotalWork(this._pendingRequests); | |
392 } else | |
393 this._beginWrite(); | |
394 }, | |
395 | |
396 /** | |
397 * @param {!Object} entry | |
398 * @param {?string} content | |
399 */ | |
400 _onContentAvailable: function(entry, content) | |
401 { | |
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(); | |
408 this._beginWrite(); | |
409 } | |
410 }, | |
411 | |
412 _beginWrite: function() | |
413 { | |
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); | |
420 }, | |
421 | |
422 /** | |
423 * @param {!WebInspector.OutputStream} stream | |
424 * @param {string=} error | |
425 */ | |
426 _writeNextChunk: function(stream, error) | |
427 { | |
428 if (this._bytesWritten >= this._text.length || error) { | |
429 stream.close(); | |
430 this._writeProgress.done(); | |
431 return; | |
432 } | |
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); | |
438 } | |
439 } | |
OLD | NEW |