2020-08-28 19:51:55 +02:00
|
|
|
/**
|
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
|
*
|
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
*
|
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
2021-03-09 04:49:57 +01:00
|
|
|
import fs from 'fs';
|
2021-02-11 15:36:15 +01:00
|
|
|
import path from 'path';
|
2021-04-25 05:39:48 +02:00
|
|
|
import util from 'util';
|
|
|
|
|
import yazl from 'yazl';
|
2021-04-27 20:07:07 +02:00
|
|
|
import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../../../utils/utils';
|
2021-04-25 05:39:48 +02:00
|
|
|
import { Artifact } from '../../artifact';
|
2021-03-10 20:43:26 +01:00
|
|
|
import { BrowserContext } from '../../browserContext';
|
2021-02-24 22:39:51 +01:00
|
|
|
import { Dialog } from '../../dialog';
|
2021-03-10 20:43:26 +01:00
|
|
|
import { ElementHandle } from '../../dom';
|
2021-02-24 22:39:51 +01:00
|
|
|
import { Frame, NavigationEvent } from '../../frames';
|
2021-03-09 04:49:57 +01:00
|
|
|
import { helper, RegisteredListener } from '../../helper';
|
2021-02-24 22:39:51 +01:00
|
|
|
import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrumentation';
|
2021-03-09 04:49:57 +01:00
|
|
|
import { Page } from '../../page';
|
|
|
|
|
import * as trace from '../common/traceEvents';
|
2021-04-24 05:39:09 +02:00
|
|
|
import { TraceSnapshotter } from './traceSnapshotter';
|
2020-08-28 19:51:55 +02:00
|
|
|
|
|
|
|
|
const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
|
2021-04-25 05:39:48 +02:00
|
|
|
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
|
|
|
|
|
const fsMkdirAsync = util.promisify(fs.mkdir.bind(fs));
|
|
|
|
|
|
|
|
|
|
export type TracerOptions = {
|
|
|
|
|
name?: string;
|
|
|
|
|
snapshots?: boolean;
|
|
|
|
|
screenshots?: boolean;
|
|
|
|
|
};
|
2020-08-28 19:51:55 +02:00
|
|
|
|
2021-04-27 20:07:07 +02:00
|
|
|
export class Tracing implements InstrumentationListener {
|
2021-04-25 05:39:48 +02:00
|
|
|
private _appendEventChain = Promise.resolve();
|
2021-04-27 20:07:07 +02:00
|
|
|
private _snapshotter: TraceSnapshotter;
|
2021-04-24 05:39:09 +02:00
|
|
|
private _eventListeners: RegisteredListener[] = [];
|
2021-04-23 18:28:18 +02:00
|
|
|
private _pendingCalls = new Map<string, { sdkObject: SdkObject, metadata: CallMetadata }>();
|
2021-04-24 03:34:52 +02:00
|
|
|
private _context: BrowserContext;
|
2021-04-25 05:39:48 +02:00
|
|
|
private _traceFile: string | undefined;
|
2021-04-27 20:07:07 +02:00
|
|
|
private _resourcesDir: string;
|
2021-04-25 05:39:48 +02:00
|
|
|
private _sha1s: string[] = [];
|
|
|
|
|
private _started = false;
|
|
|
|
|
private _traceDir: string | undefined;
|
2020-08-28 19:51:55 +02:00
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
constructor(context: BrowserContext) {
|
2021-04-24 03:34:52 +02:00
|
|
|
this._context = context;
|
2021-04-25 05:39:48 +02:00
|
|
|
this._traceDir = context._browser.options.traceDir;
|
2021-04-27 20:07:07 +02:00
|
|
|
this._resourcesDir = path.join(this._traceDir || '', 'resources');
|
|
|
|
|
this._snapshotter = new TraceSnapshotter(this._context, this._resourcesDir, traceEvent => this._appendTraceEvent(traceEvent));
|
2021-04-24 05:39:09 +02:00
|
|
|
}
|
|
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
async start(options: TracerOptions): Promise<void> {
|
2021-04-27 20:07:07 +02:00
|
|
|
// context + page must be the first events added, this method can't have awaits before them.
|
2021-04-25 05:39:48 +02:00
|
|
|
if (!this._traceDir)
|
|
|
|
|
throw new Error('Tracing directory is not specified when launching the browser');
|
|
|
|
|
if (this._started)
|
|
|
|
|
throw new Error('Tracing has already been started');
|
|
|
|
|
this._started = true;
|
|
|
|
|
this._traceFile = path.join(this._traceDir, (options.name || createGuid()) + '.trace');
|
|
|
|
|
|
|
|
|
|
this._appendEventChain = mkdirIfNeeded(this._traceFile);
|
2021-01-16 03:30:55 +01:00
|
|
|
const event: trace.ContextCreatedTraceEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
2021-04-24 05:39:09 +02:00
|
|
|
type: 'context-metadata',
|
|
|
|
|
browserName: this._context._browser.options.name,
|
|
|
|
|
isMobile: !!this._context._options.isMobile,
|
|
|
|
|
deviceScaleFactor: this._context._options.deviceScaleFactor || 1,
|
|
|
|
|
viewportSize: this._context._options.viewport || undefined,
|
|
|
|
|
debugName: this._context._options._debugName,
|
2020-08-28 19:51:55 +02:00
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
2021-04-27 20:07:07 +02:00
|
|
|
for (const page of this._context.pages())
|
|
|
|
|
this._onPage(options.screenshots, page);
|
2021-04-25 05:39:48 +02:00
|
|
|
this._eventListeners.push(
|
|
|
|
|
helper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this, options.screenshots)),
|
|
|
|
|
);
|
2021-04-27 20:07:07 +02:00
|
|
|
|
|
|
|
|
// context + page must be the first events added, no awaits above this line.
|
|
|
|
|
await fsMkdirAsync(this._resourcesDir, { recursive: true });
|
|
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
this._context.instrumentation.addListener(this);
|
|
|
|
|
if (options.snapshots)
|
2021-04-27 20:07:07 +02:00
|
|
|
await this._snapshotter.start();
|
2021-04-24 05:39:09 +02:00
|
|
|
}
|
|
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
async stop(): Promise<void> {
|
|
|
|
|
if (!this._started)
|
|
|
|
|
return;
|
|
|
|
|
this._started = false;
|
2021-04-24 05:39:09 +02:00
|
|
|
this._context.instrumentation.removeListener(this);
|
|
|
|
|
helper.removeEventListeners(this._eventListeners);
|
|
|
|
|
for (const { sdkObject, metadata } of this._pendingCalls.values())
|
2021-04-29 18:28:19 +02:00
|
|
|
await this.onAfterCall(sdkObject, metadata);
|
2021-04-25 05:39:48 +02:00
|
|
|
for (const page of this._context.pages())
|
|
|
|
|
page.setScreencastEnabled(false);
|
2021-04-24 05:39:09 +02:00
|
|
|
|
|
|
|
|
// Ensure all writes are finished.
|
|
|
|
|
await this._appendEventChain;
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
|
|
|
|
|
2021-04-27 20:07:07 +02:00
|
|
|
async dispose() {
|
|
|
|
|
await this._snapshotter.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
async export(): Promise<Artifact> {
|
|
|
|
|
if (!this._traceFile)
|
|
|
|
|
throw new Error('Tracing directory is not specified when launching the browser');
|
|
|
|
|
const zipFile = new yazl.ZipFile();
|
|
|
|
|
zipFile.addFile(this._traceFile, 'trace.trace');
|
|
|
|
|
const zipFileName = this._traceFile + '.zip';
|
|
|
|
|
for (const sha1 of this._sha1s)
|
|
|
|
|
zipFile.addFile(path.join(this._resourcesDir!, sha1), path.join('resources', sha1));
|
|
|
|
|
const zipPromise = new Promise(f => {
|
|
|
|
|
zipFile.outputStream.pipe(fs.createWriteStream(zipFileName)).on('close', f);
|
|
|
|
|
});
|
|
|
|
|
zipFile.end();
|
|
|
|
|
await zipPromise;
|
|
|
|
|
const artifact = new Artifact(this._context, zipFileName);
|
|
|
|
|
artifact.reportFinished();
|
|
|
|
|
return artifact;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-29 18:28:19 +02:00
|
|
|
async _captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) {
|
2021-02-09 23:44:48 +01:00
|
|
|
if (!sdkObject.attribution.page)
|
|
|
|
|
return;
|
2021-04-25 05:39:48 +02:00
|
|
|
if (!this._snapshotter)
|
|
|
|
|
return;
|
2021-03-09 04:49:57 +01:00
|
|
|
const snapshotName = `${name}@${metadata.id}`;
|
2021-04-23 18:28:18 +02:00
|
|
|
metadata.snapshots.push({ title: name, snapshotName });
|
2021-04-29 18:28:19 +02:00
|
|
|
await this._snapshotter!.captureSnapshot(sdkObject.attribution.page, snapshotName, element);
|
2021-01-26 03:44:46 +01:00
|
|
|
}
|
|
|
|
|
|
2021-04-24 03:34:52 +02:00
|
|
|
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
2021-04-29 18:28:19 +02:00
|
|
|
await this._captureSnapshot('before', sdkObject, metadata);
|
2021-04-23 18:28:18 +02:00
|
|
|
this._pendingCalls.set(metadata.id, { sdkObject, metadata });
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-24 03:34:52 +02:00
|
|
|
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
2021-04-29 18:28:19 +02:00
|
|
|
await this._captureSnapshot('action', sdkObject, metadata, element);
|
2021-04-23 18:28:18 +02:00
|
|
|
}
|
|
|
|
|
|
2021-04-24 03:34:52 +02:00
|
|
|
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
2021-04-24 05:39:09 +02:00
|
|
|
if (!this._pendingCalls.has(metadata.id))
|
|
|
|
|
return;
|
2021-04-29 18:28:19 +02:00
|
|
|
this._pendingCalls.delete(metadata.id);
|
2021-02-09 23:44:48 +01:00
|
|
|
if (!sdkObject.attribution.page)
|
|
|
|
|
return;
|
2021-04-29 18:28:19 +02:00
|
|
|
await this._captureSnapshot('after', sdkObject, metadata);
|
2021-01-26 03:44:46 +01:00
|
|
|
const event: trace.ActionTraceEvent = {
|
2021-04-23 18:28:18 +02:00
|
|
|
timestamp: metadata.startTime,
|
2021-01-26 03:44:46 +01:00
|
|
|
type: 'action',
|
2021-03-10 20:43:26 +01:00
|
|
|
metadata,
|
2021-04-23 18:28:18 +02:00
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onEvent(sdkObject: SdkObject, metadata: CallMetadata) {
|
|
|
|
|
if (!sdkObject.attribution.page)
|
|
|
|
|
return;
|
|
|
|
|
const event: trace.ActionTraceEvent = {
|
|
|
|
|
timestamp: metadata.startTime,
|
|
|
|
|
type: 'event',
|
|
|
|
|
metadata,
|
2021-01-26 03:44:46 +01:00
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
2020-09-11 06:42:09 +02:00
|
|
|
}
|
|
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
private _onPage(screenshots: boolean | undefined, page: Page) {
|
2021-04-21 08:03:56 +02:00
|
|
|
const pageId = page.guid;
|
2020-09-11 20:34:53 +02:00
|
|
|
|
2021-01-16 03:30:55 +01:00
|
|
|
const event: trace.PageCreatedTraceEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
2020-09-11 20:34:53 +02:00
|
|
|
type: 'page-created',
|
|
|
|
|
pageId,
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
2021-04-25 05:39:48 +02:00
|
|
|
if (screenshots)
|
|
|
|
|
page.setScreencastEnabled(true);
|
2020-09-11 20:34:53 +02:00
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
this._eventListeners.push(
|
|
|
|
|
helper.addEventListener(page, Page.Events.Dialog, (dialog: Dialog) => {
|
|
|
|
|
const event: trace.DialogOpenedEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'dialog-opened',
|
|
|
|
|
pageId,
|
|
|
|
|
dialogType: dialog.type(),
|
|
|
|
|
message: dialog.message(),
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
}),
|
2021-01-16 03:30:55 +01:00
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
helper.addEventListener(page, Page.Events.InternalDialogClosed, (dialog: Dialog) => {
|
|
|
|
|
const event: trace.DialogClosedEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'dialog-closed',
|
|
|
|
|
pageId,
|
|
|
|
|
dialogType: dialog.type(),
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
}),
|
2021-01-16 03:30:55 +01:00
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
helper.addEventListener(page.mainFrame(), Frame.Events.Navigation, (navigationEvent: NavigationEvent) => {
|
|
|
|
|
if (page.mainFrame().url() === 'about:blank')
|
|
|
|
|
return;
|
|
|
|
|
const event: trace.NavigationEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'navigation',
|
|
|
|
|
pageId,
|
|
|
|
|
url: navigationEvent.url,
|
|
|
|
|
sameDocument: !navigationEvent.newDocument,
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
}),
|
2021-01-16 03:30:55 +01:00
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
helper.addEventListener(page, Page.Events.Load, () => {
|
|
|
|
|
if (page.mainFrame().url() === 'about:blank')
|
|
|
|
|
return;
|
|
|
|
|
const event: trace.LoadEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'load',
|
|
|
|
|
pageId,
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
}),
|
2021-01-16 03:30:55 +01:00
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
helper.addEventListener(page, Page.Events.ScreencastFrame, params => {
|
2021-04-27 20:07:07 +02:00
|
|
|
const sha1 = calculateSha1(createGuid()); // no need to compute sha1 for screenshots
|
2021-04-25 05:39:48 +02:00
|
|
|
const event: trace.ScreencastFrameTraceEvent = {
|
|
|
|
|
type: 'screencast-frame',
|
|
|
|
|
pageId: page.guid,
|
2021-04-27 20:07:07 +02:00
|
|
|
sha1,
|
2021-04-25 05:39:48 +02:00
|
|
|
pageTimestamp: params.timestamp,
|
|
|
|
|
width: params.width,
|
|
|
|
|
height: params.height,
|
|
|
|
|
timestamp: monotonicTime()
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
this._appendEventChain = this._appendEventChain.then(async () => {
|
2021-04-27 20:07:07 +02:00
|
|
|
await fsWriteFileAsync(path.join(this._resourcesDir!, sha1), params.buffer).catch(() => {});
|
2021-04-25 05:39:48 +02:00
|
|
|
});
|
|
|
|
|
}),
|
2021-04-07 23:32:12 +02:00
|
|
|
|
2021-04-25 05:39:48 +02:00
|
|
|
helper.addEventListener(page, Page.Events.Close, () => {
|
|
|
|
|
const event: trace.PageDestroyedTraceEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'page-destroyed',
|
|
|
|
|
pageId,
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
})
|
|
|
|
|
);
|
2020-09-11 20:34:53 +02:00
|
|
|
}
|
|
|
|
|
|
2020-08-28 19:51:55 +02:00
|
|
|
private _appendTraceEvent(event: any) {
|
2021-04-25 05:39:48 +02:00
|
|
|
const visit = (object: any) => {
|
|
|
|
|
if (Array.isArray(object)) {
|
|
|
|
|
object.forEach(visit);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (typeof object === 'object') {
|
|
|
|
|
for (const key in object) {
|
|
|
|
|
if (key === 'sha1' || key.endsWith('Sha1')) {
|
|
|
|
|
const sha1 = object[key];
|
|
|
|
|
if (sha1)
|
|
|
|
|
this._sha1s.push(sha1);
|
|
|
|
|
}
|
|
|
|
|
visit(object[key]);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
visit(event);
|
|
|
|
|
|
2020-08-28 19:51:55 +02:00
|
|
|
// Serialize all writes to the trace file.
|
2021-04-25 05:39:48 +02:00
|
|
|
this._appendEventChain = this._appendEventChain.then(async () => {
|
|
|
|
|
await fsAppendFileAsync(this._traceFile!, JSON.stringify(event) + '\n');
|
2020-08-28 19:51:55 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|