HAREntry.js 13 KB

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