blob: cf907216d8561e88e2071480260923e95df1e260 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// See http://www.softwareishard.com/blog/har-12-spec/
// for HAR specification.
// FIXME: Some fields are not yet supported due to back-end limitations.
// See https://bugs.webkit.org/show_bug.cgi?id=58127 for details.
/**
* @unrestricted
*/
BrowserSDK.HAREntry = class {
/**
* @param {!SDK.NetworkRequest} request
*/
constructor(request) {
this._request = request;
}
/**
* @param {number} time
* @return {number}
*/
static _toMilliseconds(time) {
return time === -1 ? -1 : time * 1000;
}
/**
* @param {!SDK.NetworkRequest} request
* @return {!Promise<!Object>}
*/
static async build(request) {
const harEntry = new BrowserSDK.HAREntry(request);
let ipAddress = harEntry._request.remoteAddress();
const portPositionInString = ipAddress.lastIndexOf(':');
if (portPositionInString !== -1)
ipAddress = ipAddress.substr(0, portPositionInString);
const timings = harEntry._buildTimings();
let time = 0;
// "ssl" is included in the connect field, so do not double count it.
for (const t of [timings.blocked, timings.dns, timings.connect, timings.send, timings.wait, timings.receive])
time += Math.max(t, 0);
const entry = {
startedDateTime: BrowserSDK.HARLog.pseudoWallTime(harEntry._request, harEntry._request.issueTime()).toJSON(),
time: time,
request: await harEntry._buildRequest(),
response: harEntry._buildResponse(),
cache: {}, // Not supported yet.
timings: timings,
// IPv6 address should not have square brackets per (https://tools.ietf.org/html/rfc2373#section-2.2).
serverIPAddress: ipAddress.replace(/\[\]/g, '')
};
// Chrome specific.
if (harEntry._request.cached())
entry._fromCache = harEntry._request.cachedInMemory() ? 'memory' : 'disk';
if (harEntry._request.connectionId !== '0')
entry.connection = harEntry._request.connectionId;
const page = BrowserSDK.PageLoad.forRequest(harEntry._request);
if (page)
entry.pageref = 'page_' + page.id;
return entry;
}
/**
* @return {!Promise<!Object>}
*/
async _buildRequest() {
const headersText = this._request.requestHeadersText();
const res = {
method: this._request.requestMethod,
url: this._buildRequestURL(this._request.url()),
httpVersion: this._request.requestHttpVersion(),
headers: this._request.requestHeaders(),
queryString: this._buildParameters(this._request.queryParameters || []),
cookies: this._buildCookies(this._request.requestCookies || []),
headersSize: headersText ? headersText.length : -1,
bodySize: this.requestBodySize
};
const postData = await this._buildPostData();
if (postData)
res.postData = postData;
return res;
}
/**
* @return {!Object}
*/
_buildResponse() {
const headersText = this._request.responseHeadersText;
return {
status: this._request.statusCode,
statusText: this._request.statusText,
httpVersion: this._request.responseHttpVersion(),
headers: this._request.responseHeaders,
cookies: this._buildCookies(this._request.responseCookies || []),
content: this._buildContent(),
redirectURL: this._request.responseHeaderValue('Location') || '',
headersSize: headersText ? headersText.length : -1,
bodySize: this.responseBodySize,
_transferSize: this._request.transferSize,
_error: this._request.localizedFailDescription
};
}
/**
* @return {!Object}
*/
_buildContent() {
const content = {
size: this._request.resourceSize,
mimeType: this._request.mimeType || 'x-unknown',
// 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)
};
const compression = this.responseCompression;
if (typeof compression === 'number')
content.compression = compression;
return content;
}
/**
* @return {!BrowserSDK.HAREntry.Timing}
*/
_buildTimings() {
// Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], duration
const timing = this._request.timing;
const issueTime = this._request.issueTime();
const startTime = this._request.startTime;
const result = {blocked: -1, dns: -1, ssl: -1, connect: -1, send: 0, wait: 0, receive: 0, _blocked_queueing: -1};
const queuedTime = (issueTime < startTime) ? startTime - issueTime : -1;
result.blocked = queuedTime;
result._blocked_queueing = BrowserSDK.HAREntry._toMilliseconds(queuedTime);
let highestTime = 0;
if (timing) {
// "blocked" here represents both queued + blocked/stalled + proxy (ie: anything before request was started).
// We pick the better of when the network request start was reported and pref timing.
const blockedStart = leastNonNegative([timing.dnsStart, timing.connectStart, timing.sendStart]);
if (blockedStart !== Infinity)
result.blocked += blockedStart;
// Proxy is part of blocked but sometimes (like quic) blocked is -1 but has proxy timings.
if (timing.proxyEnd !== -1)
result._blocked_proxy = timing.proxyEnd - timing.proxyStart;
if (result._blocked_proxy && result._blocked_proxy > result.blocked)
result.blocked = result._blocked_proxy;
const dnsStart = timing.dnsEnd >= 0 ? blockedStart : 0;
const dnsEnd = timing.dnsEnd >= 0 ? timing.dnsEnd : -1;
result.dns = dnsEnd - dnsStart;
// SSL timing is included in connection timing.
const sslStart = timing.sslEnd > 0 ? timing.sslStart : 0;
const sslEnd = timing.sslEnd > 0 ? timing.sslEnd : -1;
result.ssl = sslEnd - sslStart;
const connectStart = timing.connectEnd >= 0 ? leastNonNegative([dnsEnd, blockedStart]) : 0;
const connectEnd = timing.connectEnd >= 0 ? timing.connectEnd : -1;
result.connect = connectEnd - connectStart;
// Send should not be -1 for legacy reasons even if it is served from cache.
const sendStart = timing.sendEnd >= 0 ? Math.max(connectEnd, dnsEnd, blockedStart) : 0;
const sendEnd = timing.sendEnd >= 0 ? timing.sendEnd : 0;
result.send = sendEnd - sendStart;
// Quic sometimes says that sendStart is before connectionEnd (see: crbug.com/740792)
if (result.send < 0)
result.send = 0;
highestTime = Math.max(sendEnd, connectEnd, sslEnd, dnsEnd, blockedStart, 0);
} else if (this._request.responseReceivedTime === -1) {
// Means that we don't have any more details after blocked, so attribute all to blocked.
result.blocked = this._request.endTime - issueTime;
return result;
}
const requestTime = timing ? timing.requestTime : startTime;
const waitStart = highestTime;
const waitEnd = BrowserSDK.HAREntry._toMilliseconds(this._request.responseReceivedTime - requestTime);
result.wait = waitEnd - waitStart;
const receiveStart = waitEnd;
const receiveEnd = BrowserSDK.HAREntry._toMilliseconds(this._request.endTime - issueTime);
result.receive = Math.max(receiveEnd - receiveStart, 0);
return result;
/**
* @param {!Array<number>} values
* @return {number}
*/
function leastNonNegative(values) {
return values.reduce((best, value) => (value >= 0 && value < best) ? value : best, Infinity);
}
}
/**
* @return {!Promise<!Object>}
*/
async _buildPostData() {
const postData = await this._request.requestFormData();
if (!postData)
return null;
const res = {mimeType: this._request.requestContentType(), text: postData};
const formParameters = await this._request.formParameters();
if (formParameters)
res.params = this._buildParameters(formParameters);
return res;
}
/**
* @param {!Array.<!Object>} parameters
* @return {!Array.<!Object>}
*/
_buildParameters(parameters) {
return parameters.slice();
}
/**
* @param {string} url
* @return {string}
*/
_buildRequestURL(url) {
return url.split('#', 2)[0];
}
/**
* @param {!Array.<!SDK.Cookie>} cookies
* @return {!Array.<!Object>}
*/
_buildCookies(cookies) {
return cookies.map(this._buildCookie.bind(this));
}
/**
* @param {!SDK.Cookie} cookie
* @return {!Object}
*/
_buildCookie(cookie) {
const c = {
name: cookie.name(),
value: cookie.value(),
path: cookie.path(),
domain: cookie.domain(),
expires: cookie.expiresDate(BrowserSDK.HARLog.pseudoWallTime(this._request, this._request.startTime)),
httpOnly: cookie.httpOnly(),
secure: cookie.secure()
};
if (cookie.sameSite())
c.sameSite = cookie.sameSite();
return c;
}
/**
* @return {number}
*/
get requestBodySize() {
return !this._request.requestFormData ? 0 : this._request.requestFormData.length;
}
/**
* @return {number}
*/
get responseBodySize() {
if (this._request.cached() || this._request.statusCode === 304)
return 0;
if (!this._request.responseHeadersText)
return -1;
return this._request.transferSize - this._request.responseHeadersText.length;
}
/**
* @return {number|undefined}
*/
get responseCompression() {
if (this._request.cached() || this._request.statusCode === 304 || this._request.statusCode === 206)
return;
if (!this._request.responseHeadersText)
return;
return this._request.resourceSize - this.responseBodySize;
}
};
/** @typedef {!{
blocked: number,
dns: number,
ssl: number,
connect: number,
send: number,
wait: number,
receive: number,
_blocked_queueing: number,
_blocked_proxy: (number|undefined)
}} */
BrowserSDK.HAREntry.Timing;
/**
* @unrestricted
*/
BrowserSDK.HARLog = class {
/**
* @param {!SDK.NetworkRequest} request
* @param {number} monotonicTime
* @return {!Date}
*/
static pseudoWallTime(request, monotonicTime) {
return new Date(request.pseudoWallTime(monotonicTime) * 1000);
}
/**
* @param {!Array.<!SDK.NetworkRequest>} requests
* @return {!Promise<!Object>}
*/
static async build(requests) {
const log = new BrowserSDK.HARLog();
const entryPromises = [];
for (const request of requests)
entryPromises.push(BrowserSDK.HAREntry.build(request));
const entries = await Promise.all(entryPromises);
return {version: '1.2', creator: log._creator(), pages: log._buildPages(requests), entries: entries};
}
_creator() {
const webKitVersion = /AppleWebKit\/([^ ]+)/.exec(window.navigator.userAgent);
return {name: 'WebInspector', version: webKitVersion ? webKitVersion[1] : 'n/a'};
}
/**
* @param {!Array.<!SDK.NetworkRequest>} requests
* @return {!Array.<!Object>}
*/
_buildPages(requests) {
const seenIdentifiers = {};
const pages = [];
for (let i = 0; i < requests.length; ++i) {
const request = requests[i];
const page = BrowserSDK.PageLoad.forRequest(request);
if (!page || seenIdentifiers[page.id])
continue;
seenIdentifiers[page.id] = true;
pages.push(this._convertPage(page, request));
}
return pages;
}
/**
* @param {!BrowserSDK.PageLoad} page
* @param {!SDK.NetworkRequest} request
* @return {!Object}
*/
_convertPage(page, request) {
return {
startedDateTime: BrowserSDK.HARLog.pseudoWallTime(request, page.startTime).toJSON(),
id: 'page_' + page.id,
title: page.url, // We don't have actual page title here. URL is probably better than nothing.
pageTimings: {
onContentLoad: this._pageEventTime(page, page.contentLoadTime),
onLoad: this._pageEventTime(page, page.loadTime)
}
};
}
/**
* @param {!BrowserSDK.PageLoad} page
* @param {number} time
* @return {number}
*/
_pageEventTime(page, time) {
const startTime = page.startTime;
if (time === -1 || startTime === -1)
return -1;
return BrowserSDK.HAREntry._toMilliseconds(time - startTime);
}
};