HAREntry.js 13 KB

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