| 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 |