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-02-24 22:39:51 +01:00
|
|
|
import { BrowserContext, Video } from '../../browserContext';
|
2021-02-25 18:33:32 +01:00
|
|
|
import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from '../../snapshot/snapshotter';
|
2021-02-24 22:39:51 +01:00
|
|
|
import * as trace from '../common/traceEvents';
|
2021-02-11 15:36:15 +01:00
|
|
|
import path from 'path';
|
2020-08-28 19:51:55 +02:00
|
|
|
import * as util from 'util';
|
2021-02-11 15:36:15 +01:00
|
|
|
import fs from 'fs';
|
2021-02-24 22:39:51 +01:00
|
|
|
import { createGuid, getFromENV, mkdirIfNeeded, monotonicTime } from '../../../utils/utils';
|
|
|
|
|
import { Page } from '../../page';
|
2021-02-25 18:33:32 +01:00
|
|
|
import { Snapshotter } from '../../snapshot/snapshotter';
|
2021-02-24 22:39:51 +01:00
|
|
|
import { helper, RegisteredListener } from '../../helper';
|
|
|
|
|
import { Dialog } from '../../dialog';
|
|
|
|
|
import { Frame, NavigationEvent } from '../../frames';
|
|
|
|
|
import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrumentation';
|
2021-02-25 18:33:32 +01:00
|
|
|
import { FrameSnapshot } from '../../snapshot/snapshot';
|
2020-08-28 19:51:55 +02:00
|
|
|
|
|
|
|
|
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
|
|
|
|
|
const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
|
|
|
|
|
const fsAccessAsync = util.promisify(fs.access.bind(fs));
|
2020-12-01 00:25:07 +01:00
|
|
|
const envTrace = getFromENV('PW_TRACE_DIR');
|
2020-08-28 19:51:55 +02:00
|
|
|
|
2021-02-09 23:44:48 +01:00
|
|
|
export class Tracer implements InstrumentationListener {
|
2020-09-18 20:54:00 +02:00
|
|
|
private _contextTracers = new Map<BrowserContext, ContextTracer>();
|
2020-09-05 01:31:52 +02:00
|
|
|
|
2020-09-18 20:54:00 +02:00
|
|
|
async onContextCreated(context: BrowserContext): Promise<void> {
|
2021-01-21 04:16:23 +01:00
|
|
|
const traceDir = envTrace || context._options._traceDir;
|
|
|
|
|
if (!traceDir)
|
2020-09-18 20:54:00 +02:00
|
|
|
return;
|
2021-01-21 04:16:23 +01:00
|
|
|
const traceStorageDir = path.join(traceDir, 'resources');
|
|
|
|
|
const tracePath = path.join(traceDir, createGuid() + '.trace');
|
2020-12-01 00:25:07 +01:00
|
|
|
const contextTracer = new ContextTracer(context, traceStorageDir, tracePath);
|
2021-02-26 23:16:32 +01:00
|
|
|
await contextTracer.start();
|
2020-09-18 20:54:00 +02:00
|
|
|
this._contextTracers.set(context, contextTracer);
|
2020-09-05 01:31:52 +02:00
|
|
|
}
|
|
|
|
|
|
2020-10-26 22:32:07 +01:00
|
|
|
async onContextDidDestroy(context: BrowserContext): Promise<void> {
|
2020-09-18 20:54:00 +02:00
|
|
|
const contextTracer = this._contextTracers.get(context);
|
|
|
|
|
if (contextTracer) {
|
|
|
|
|
await contextTracer.dispose().catch(e => {});
|
|
|
|
|
this._contextTracers.delete(context);
|
2020-09-12 00:13:37 +02:00
|
|
|
}
|
2020-09-05 01:31:52 +02:00
|
|
|
}
|
2021-02-09 23:44:48 +01:00
|
|
|
|
2021-02-11 06:55:46 +01:00
|
|
|
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
|
|
|
|
this._contextTracers.get(sdkObject.attribution.context!)?.onActionCheckpoint('before', sdkObject, metadata);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async onAfterInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
|
|
|
|
this._contextTracers.get(sdkObject.attribution.context!)?.onActionCheckpoint('after', sdkObject, metadata);
|
2021-02-09 23:44:48 +01:00
|
|
|
}
|
|
|
|
|
|
2021-02-11 06:55:46 +01:00
|
|
|
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
|
|
|
|
this._contextTracers.get(sdkObject.attribution.context!)?.onAfterCall(sdkObject, metadata);
|
2021-02-09 23:44:48 +01:00
|
|
|
}
|
2020-09-05 01:31:52 +02:00
|
|
|
}
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
const snapshotsSymbol = Symbol('snapshots');
|
|
|
|
|
|
2021-02-11 06:55:46 +01:00
|
|
|
// This is an official way to pass snapshots between onBefore/AfterInputAction and onAfterCall.
|
2021-02-09 23:44:48 +01:00
|
|
|
function snapshotsForMetadata(metadata: CallMetadata): { name: string, snapshotId: string }[] {
|
2021-01-26 03:44:46 +01:00
|
|
|
if (!(metadata as any)[snapshotsSymbol])
|
|
|
|
|
(metadata as any)[snapshotsSymbol] = [];
|
|
|
|
|
return (metadata as any)[snapshotsSymbol];
|
|
|
|
|
}
|
2021-01-19 23:45:26 +01:00
|
|
|
|
2021-02-09 23:44:48 +01:00
|
|
|
class ContextTracer implements SnapshotterDelegate {
|
2020-09-05 01:31:52 +02:00
|
|
|
private _contextId: string;
|
2020-08-28 19:51:55 +02:00
|
|
|
private _traceStoragePromise: Promise<string>;
|
|
|
|
|
private _appendEventChain: Promise<string>;
|
|
|
|
|
private _writeArtifactChain: Promise<void>;
|
2020-09-11 20:34:53 +02:00
|
|
|
private _snapshotter: Snapshotter;
|
|
|
|
|
private _eventListeners: RegisteredListener[];
|
|
|
|
|
private _disposed = false;
|
2020-12-14 22:31:55 +01:00
|
|
|
private _traceFile: string;
|
2020-08-28 19:51:55 +02:00
|
|
|
|
2020-09-05 01:31:52 +02:00
|
|
|
constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) {
|
|
|
|
|
this._contextId = 'context@' + createGuid();
|
2020-12-14 22:31:55 +01:00
|
|
|
this._traceFile = traceFile;
|
2020-08-28 19:51:55 +02:00
|
|
|
this._traceStoragePromise = mkdirIfNeeded(path.join(traceStorageDir, 'sha1')).then(() => traceStorageDir);
|
|
|
|
|
this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile);
|
|
|
|
|
this._writeArtifactChain = Promise.resolve();
|
2021-01-16 03:30:55 +01:00
|
|
|
const event: trace.ContextCreatedTraceEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
2020-08-28 19:51:55 +02:00
|
|
|
type: 'context-created',
|
2021-01-30 01:00:56 +01:00
|
|
|
browserName: context._browser.options.name,
|
2020-09-05 01:31:52 +02:00
|
|
|
contextId: this._contextId,
|
2020-08-28 19:51:55 +02:00
|
|
|
isMobile: !!context._options.isMobile,
|
|
|
|
|
deviceScaleFactor: context._options.deviceScaleFactor || 1,
|
|
|
|
|
viewportSize: context._options.viewport || undefined,
|
2021-01-28 19:50:57 +01:00
|
|
|
debugName: context._options._debugName,
|
2020-08-28 19:51:55 +02:00
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
2020-09-05 01:31:52 +02:00
|
|
|
this._snapshotter = new Snapshotter(context, this);
|
2020-09-11 20:34:53 +02:00
|
|
|
this._eventListeners = [
|
|
|
|
|
helper.addEventListener(context, BrowserContext.Events.Page, this._onPage.bind(this)),
|
|
|
|
|
];
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
|
|
|
|
|
2021-02-26 23:16:32 +01:00
|
|
|
async start() {
|
|
|
|
|
await this._snapshotter.start();
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-05 01:31:52 +02:00
|
|
|
onBlob(blob: SnapshotterBlob): void {
|
2020-08-28 19:51:55 +02:00
|
|
|
this._writeArtifact(blob.sha1, blob.buffer);
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-14 16:56:04 +02:00
|
|
|
onResource(resource: SnapshotterResource): void {
|
2021-01-16 03:30:55 +01:00
|
|
|
const event: trace.NetworkResourceTraceEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
2020-08-28 19:51:55 +02:00
|
|
|
type: 'resource',
|
2020-09-05 01:31:52 +02:00
|
|
|
contextId: this._contextId,
|
2020-09-14 16:56:04 +02:00
|
|
|
pageId: resource.pageId,
|
2020-08-28 19:51:55 +02:00
|
|
|
frameId: resource.frameId,
|
2021-02-25 22:09:26 +01:00
|
|
|
resourceId: resource.resourceId,
|
2020-08-28 19:51:55 +02:00
|
|
|
url: resource.url,
|
|
|
|
|
contentType: resource.contentType,
|
|
|
|
|
responseHeaders: resource.responseHeaders,
|
2021-01-26 20:06:05 +01:00
|
|
|
requestHeaders: resource.requestHeaders,
|
|
|
|
|
method: resource.method,
|
|
|
|
|
status: resource.status,
|
|
|
|
|
requestSha1: resource.requestSha1,
|
|
|
|
|
responseSha1: resource.responseSha1,
|
2020-08-28 19:51:55 +02:00
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-25 22:09:26 +01:00
|
|
|
onFrameSnapshot(snapshot: FrameSnapshot): void {
|
2021-01-26 03:44:46 +01:00
|
|
|
const event: trace.FrameSnapshotTraceEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'snapshot',
|
|
|
|
|
contextId: this._contextId,
|
2021-02-25 22:09:26 +01:00
|
|
|
pageId: snapshot.pageId,
|
|
|
|
|
frameId: snapshot.frameId,
|
2021-01-28 04:42:51 +01:00
|
|
|
snapshot: snapshot,
|
2021-01-26 03:44:46 +01:00
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-09 23:44:48 +01:00
|
|
|
async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
|
|
|
|
if (!sdkObject.attribution.page)
|
|
|
|
|
return;
|
2021-01-26 03:44:46 +01:00
|
|
|
const snapshotId = createGuid();
|
|
|
|
|
snapshotsForMetadata(metadata).push({ name, snapshotId });
|
2021-02-09 23:44:48 +01:00
|
|
|
await this._snapshotter.forceSnapshot(sdkObject.attribution.page, snapshotId);
|
2021-01-26 03:44:46 +01:00
|
|
|
}
|
|
|
|
|
|
2021-02-11 06:55:46 +01:00
|
|
|
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
2021-02-09 23:44:48 +01:00
|
|
|
if (!sdkObject.attribution.page)
|
|
|
|
|
return;
|
2021-01-26 03:44:46 +01:00
|
|
|
const event: trace.ActionTraceEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'action',
|
|
|
|
|
contextId: this._contextId,
|
2021-02-25 22:09:26 +01:00
|
|
|
pageId: sdkObject.attribution.page.idInSnapshot,
|
2021-02-09 23:44:48 +01:00
|
|
|
objectType: metadata.type,
|
|
|
|
|
method: metadata.method,
|
|
|
|
|
// FIXME: filter out evaluation snippets, binary
|
|
|
|
|
params: metadata.params,
|
|
|
|
|
stack: metadata.stack,
|
2021-02-11 06:50:29 +01:00
|
|
|
startTime: metadata.startTime,
|
|
|
|
|
endTime: metadata.endTime,
|
|
|
|
|
logs: metadata.log.slice(),
|
2021-02-14 07:13:51 +01:00
|
|
|
error: metadata.error,
|
2021-01-26 03:44:46 +01:00
|
|
|
snapshots: snapshotsForMetadata(metadata),
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
2020-09-11 06:42:09 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-11 20:34:53 +02:00
|
|
|
private _onPage(page: Page) {
|
2021-02-25 22:09:26 +01:00
|
|
|
const pageId = page.idInSnapshot;
|
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',
|
|
|
|
|
contextId: this._contextId,
|
|
|
|
|
pageId,
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
|
2020-09-19 02:36:43 +02:00
|
|
|
page.on(Page.Events.VideoStarted, (video: Video) => {
|
|
|
|
|
if (this._disposed)
|
|
|
|
|
return;
|
2021-01-16 03:30:55 +01:00
|
|
|
const event: trace.PageVideoTraceEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
2020-09-19 02:36:43 +02:00
|
|
|
type: 'page-video',
|
|
|
|
|
contextId: this._contextId,
|
|
|
|
|
pageId,
|
2020-12-14 22:31:55 +01:00
|
|
|
fileName: path.relative(path.dirname(this._traceFile), video._path),
|
2020-09-19 02:36:43 +02:00
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
});
|
|
|
|
|
|
2021-01-16 03:30:55 +01:00
|
|
|
page.on(Page.Events.Dialog, (dialog: Dialog) => {
|
|
|
|
|
if (this._disposed)
|
|
|
|
|
return;
|
|
|
|
|
const event: trace.DialogOpenedEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'dialog-opened',
|
|
|
|
|
contextId: this._contextId,
|
|
|
|
|
pageId,
|
|
|
|
|
dialogType: dialog.type(),
|
|
|
|
|
message: dialog.message(),
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
page.on(Page.Events.InternalDialogClosed, (dialog: Dialog) => {
|
|
|
|
|
if (this._disposed)
|
|
|
|
|
return;
|
|
|
|
|
const event: trace.DialogClosedEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'dialog-closed',
|
|
|
|
|
contextId: this._contextId,
|
|
|
|
|
pageId,
|
|
|
|
|
dialogType: dialog.type(),
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
page.mainFrame().on(Frame.Events.Navigation, (navigationEvent: NavigationEvent) => {
|
|
|
|
|
if (this._disposed || page.mainFrame().url() === 'about:blank')
|
|
|
|
|
return;
|
|
|
|
|
const event: trace.NavigationEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'navigation',
|
|
|
|
|
contextId: this._contextId,
|
|
|
|
|
pageId,
|
|
|
|
|
url: navigationEvent.url,
|
|
|
|
|
sameDocument: !navigationEvent.newDocument,
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
page.on(Page.Events.Load, () => {
|
|
|
|
|
if (this._disposed || page.mainFrame().url() === 'about:blank')
|
|
|
|
|
return;
|
|
|
|
|
const event: trace.LoadEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
|
|
|
|
type: 'load',
|
|
|
|
|
contextId: this._contextId,
|
|
|
|
|
pageId,
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
});
|
|
|
|
|
|
2020-09-11 20:34:53 +02:00
|
|
|
page.once(Page.Events.Close, () => {
|
|
|
|
|
if (this._disposed)
|
|
|
|
|
return;
|
2021-01-16 03:30:55 +01:00
|
|
|
const event: trace.PageDestroyedTraceEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
2020-09-11 20:34:53 +02:00
|
|
|
type: 'page-destroyed',
|
|
|
|
|
contextId: this._contextId,
|
|
|
|
|
pageId,
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-28 19:51:55 +02:00
|
|
|
async dispose() {
|
2020-09-11 20:34:53 +02:00
|
|
|
this._disposed = true;
|
|
|
|
|
helper.removeEventListeners(this._eventListeners);
|
2020-09-05 01:31:52 +02:00
|
|
|
this._snapshotter.dispose();
|
2021-01-16 03:30:55 +01:00
|
|
|
const event: trace.ContextDestroyedTraceEvent = {
|
|
|
|
|
timestamp: monotonicTime(),
|
2020-09-05 01:31:52 +02:00
|
|
|
type: 'context-destroyed',
|
|
|
|
|
contextId: this._contextId,
|
|
|
|
|
};
|
|
|
|
|
this._appendTraceEvent(event);
|
|
|
|
|
|
2020-08-28 19:51:55 +02:00
|
|
|
// Ensure all writes are finished.
|
|
|
|
|
await this._appendEventChain;
|
|
|
|
|
await this._writeArtifactChain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _writeArtifact(sha1: string, buffer: Buffer) {
|
|
|
|
|
// Save all write promises to wait for them in dispose.
|
|
|
|
|
const promise = this._innerWriteArtifact(sha1, buffer);
|
|
|
|
|
this._writeArtifactChain = this._writeArtifactChain.then(() => promise);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _innerWriteArtifact(sha1: string, buffer: Buffer): Promise<void> {
|
|
|
|
|
const traceDirectory = await this._traceStoragePromise;
|
|
|
|
|
const filePath = path.join(traceDirectory, sha1);
|
|
|
|
|
try {
|
|
|
|
|
await fsAccessAsync(filePath);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// File does not exist - write it.
|
|
|
|
|
await fsWriteFileAsync(filePath, buffer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _appendTraceEvent(event: any) {
|
|
|
|
|
// Serialize all writes to the trace file.
|
|
|
|
|
this._appendEventChain = this._appendEventChain.then(async traceFile => {
|
2021-01-16 03:30:55 +01:00
|
|
|
await fsAppendFileAsync(traceFile, JSON.stringify(event) + '\n');
|
2020-08-28 19:51:55 +02:00
|
|
|
return traceFile;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|