chrome-har-capturer
Version:
Capture HAR files from a headless Chrome instance
139 lines (130 loc) • 4.81 kB
JavaScript
;
const Context = require('./context');
const Stats = require('./stats');
class Timer {
constructor(milliseconds) {
this._milliseconds = milliseconds;
}
start() {
this.cancel();
return new Promise((fulfill, reject) => {
if (typeof this._milliseconds === 'undefined') {
// wait indefinitely
return;
}
this._id = setTimeout(fulfill, this._milliseconds);
});
}
cancel() {
clearTimeout(this._id);
}
}
class Live {
constructor({url, index, urls, options}) {
this._url = url;
this._index = index;
this._urls = urls;
this._options = options;
}
async load() {
// create a fresh new context for this URL
const context = new Context(this._options);
const client = await context.create();
// hooks
const {preHook, postHook} = this._options;
const hookArgs = [this._url, client, this._index, this._urls];
// optionally run the user-defined hook
if (typeof preHook === 'function') {
await preHook.apply(null, hookArgs);
}
// create (but not start) the page timer
const timer = new Timer(this._options.timeout);
// handle proper page load and postHook or related errors
const pageLoad = async () => {
try {
// start the page load and waits for its termination
const stats = await this._loadPage(client);
// optionally run the user-defined hook
if (typeof postHook === 'function') {
stats.user = await postHook.apply(null, hookArgs);
}
return stats;
} finally {
// no-matter-what cleanup functions
await context.destroy();
timer.cancel();
}
};
// handle Chrome disconnection
const disconnection = async () => {
await new Promise((fulfill, reject) => {
client.once('disconnect', fulfill);
});
timer.cancel();
throw new Error('Disconnected');
};
// handle page timeout
const timeout = async () => {
await timer.start();
await context.destroy();
throw new Error('Timed out');
};
// wait for the first event to happen
return await Promise.race([
pageLoad(),
disconnection(),
timeout()
]);
}
async _loadPage(client) {
// enable domains
const {Page, Network} = client;
await Network.enable();
await Page.enable();
// register events
const stats = new Stats(this._url, this._options);
const termination = new Promise((fulfill, reject) => {
client.on('event', (event) => {
stats.processEvent(fulfill, reject, event);
});
// XXX the separation of concerns between live fetching and HAR
// computation made it necessary to introduce a synthetic event
// which is the reply of the Network.getResponseBody method
if (this._options.content) {
Network.loadingFinished(async ({requestId}) => {
// only for those entries that are being tracked (e.g., not
// for cached items)
if (!stats.entries.get(requestId)) {
return;
}
try {
const params = await Network.getResponseBody({requestId});
const {body, base64Encoded} = params;
stats.processEvent(fulfill, reject, {
method: 'Network.getResponseBody',
params: {
requestId,
body,
base64Encoded
}
});
} catch (err) {
// sometimes it is impossible to fetch the content (see #82)
stats.processEvent(fulfill, reject, {
method: 'Network.getResponseBody',
params: {
requestId
}
});
}
});
}
});
// start the page load
const navigation = Page.navigate({url: this._url});
// events will determine termination
await Promise.all([termination, navigation]);
return stats;
}
}
module.exports = Live;