Skip to content

Commit

Permalink
feat(trace): trace page open/close events (#3852)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgozman authored Sep 11, 2020
1 parent f94df31 commit 16be357
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 56 deletions.
13 changes: 13 additions & 0 deletions src/trace/traceTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,23 @@ export type NetworkResourceTraceEvent = {
sha1: string,
};

export type PageCreatedTraceEvent = {
type: 'page-created',
contextId: string,
pageId: string,
};

export type PageDestroyedTraceEvent = {
type: 'page-destroyed',
contextId: string,
pageId: string,
};

export type ActionTraceEvent = {
type: 'action',
contextId: string,
action: string,
pageId?: string,
target?: string,
label?: string,
value?: string,
Expand Down
121 changes: 69 additions & 52 deletions src/trace/traceViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import * as path from 'path';
import * as util from 'util';
import * as fs from 'fs';
import type { NetworkResourceTraceEvent, ActionTraceEvent, ContextCreatedTraceEvent, ContextDestroyedTraceEvent } from './traceTypes';
import type { NetworkResourceTraceEvent, ActionTraceEvent, ContextCreatedTraceEvent, ContextDestroyedTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent } from './traceTypes';
import type { FrameSnapshot, PageSnapshot } from './snapshotter';
import type { Browser, BrowserContext, Frame, Page, Route } from '../client/api';
import type { Playwright } from '../client/playwright';
Expand All @@ -26,6 +26,8 @@ const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
type TraceEvent =
ContextCreatedTraceEvent |
ContextDestroyedTraceEvent |
PageCreatedTraceEvent |
PageDestroyedTraceEvent |
NetworkResourceTraceEvent |
ActionTraceEvent;

Expand Down Expand Up @@ -95,62 +97,77 @@ class TraceViewer {
data.actions.push(event);
}
}
await uiPage.evaluate(contextData => {
for (const data of Object.values(contextData)) {
const header = document.createElement('div');
header.textContent = data.label;
header.style.margin = '10px';
document.body.appendChild(header);
for (const action of data.actions) {
const div = document.createElement('div');
div.style.whiteSpace = 'pre';
div.style.borderBottom = '1px solid black';
const lines = [];
lines.push(`action: ${action.action}`);
if (action.label)
lines.push(`label: ${action.label}`);
if (action.target)
lines.push(`target: ${action.target}`);
if (action.value)
lines.push(`value: ${action.value}`);
if (action.startTime && action.endTime)
lines.push(`duration: ${action.endTime - action.startTime}ms`);
div.textContent = lines.join('\n');
if (action.error) {
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = 'error';
details.appendChild(summary);
details.appendChild(document.createTextNode(action.error));
div.appendChild(details);
}
if (action.stack) {
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = 'callstack';
details.appendChild(summary);
details.appendChild(document.createTextNode(action.stack));
div.appendChild(details);
await uiPage.evaluate(traces => {
function createSection(parent: Element, title: string): HTMLDetailsElement {
const details = document.createElement('details');
details.style.paddingLeft = '10px';
const summary = document.createElement('summary');
summary.textContent = title;
details.appendChild(summary);
parent.appendChild(details);
return details;
}

function createField(parent: Element, text: string) {
const div = document.createElement('div');
div.style.whiteSpace = 'pre';
div.textContent = text;
parent.appendChild(div);
}

for (const trace of traces) {
const traceSection = createSection(document.body, trace.traceFile);
traceSection.open = true;

const contextSections = new Map<string, Element>();
const pageSections = new Map<string, Element>();

for (const event of trace.events) {
if (event.type === 'context-created') {
const contextSection = createSection(traceSection, event.contextId);
contextSection.open = true;
contextSections.set(event.contextId, contextSection);
}
if (action.logs && action.logs.length) {
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = 'logs';
details.appendChild(summary);
details.appendChild(document.createTextNode(action.logs.join('\n')));
div.appendChild(details);
if (event.type === 'page-created') {
const contextSection = contextSections.get(event.contextId)!;
const pageSection = createSection(contextSection, event.pageId);
pageSection.open = true;
pageSections.set(event.pageId, pageSection);
}
if (action.snapshot) {
const button = document.createElement('button');
button.style.display = 'block';
button.textContent = `snapshot after (${action.snapshot.duration}ms)`;
button.addEventListener('click', () => (window as any).renderSnapshot(action));
div.appendChild(button);
if (event.type === 'action') {
const parentSection = event.pageId ? pageSections.get(event.pageId)! : contextSections.get(event.contextId)!;
const actionSection = createSection(parentSection, event.action);
if (event.label)
createField(actionSection, `label: ${event.label}`);
if (event.target)
createField(actionSection, `target: ${event.target}`);
if (event.value)
createField(actionSection, `value: ${event.value}`);
if (event.startTime && event.endTime)
createField(actionSection, `duration: ${event.endTime - event.startTime}ms`);
if (event.error) {
const errorSection = createSection(actionSection, 'error');
createField(errorSection, event.error);
}
if (event.stack) {
const errorSection = createSection(actionSection, 'stack');
createField(errorSection, event.stack);
}
if (event.logs && event.logs.length) {
const errorSection = createSection(actionSection, 'logs');
createField(errorSection, event.logs.join('\n'));
}
if (event.snapshot) {
const button = document.createElement('button');
button.style.display = 'block';
button.textContent = `snapshot after (${event.snapshot.duration}ms)`;
button.addEventListener('click', () => (window as any).renderSnapshot(event));
actionSection.appendChild(button);
}
}
document.body.appendChild(div);
}
}
}, contextData);
}, this._traces);
}

private async _ensureContext(browser: Browser, contextId: string): Promise<BrowserContext> {
Expand Down
43 changes: 39 additions & 4 deletions src/trace/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,20 @@
* limitations under the License.
*/

import type { BrowserContext } from '../server/browserContext';
import { BrowserContext } from '../server/browserContext';
import type { SanpshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent } from './traceTypes';
import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent } from './traceTypes';
import * as path from 'path';
import * as util from 'util';
import * as fs from 'fs';
import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../utils/utils';
import { ActionResult, InstrumentingAgent, instrumentingAgents, ActionMetadata } from '../server/instrumentation';
import type { Page } from '../server/page';
import { Page } from '../server/page';
import { Progress, runAbortableTask } from '../server/progress';
import { Snapshotter } from './snapshotter';
import * as types from '../server/types';
import type { ElementHandle } from '../server/dom';
import { helper, RegisteredListener } from '../server/helper';

const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
Expand Down Expand Up @@ -80,7 +81,10 @@ class ContextTracer implements SnapshotterDelegate {
private _traceStoragePromise: Promise<string>;
private _appendEventChain: Promise<string>;
private _writeArtifactChain: Promise<void>;
readonly _snapshotter: Snapshotter;
private _snapshotter: Snapshotter;
private _eventListeners: RegisteredListener[];
private _disposed = false;
private _pageToId = new Map<Page, string>();

constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) {
this._contextId = 'context@' + createGuid();
Expand All @@ -97,6 +101,9 @@ class ContextTracer implements SnapshotterDelegate {
};
this._appendTraceEvent(event);
this._snapshotter = new Snapshotter(context, this);
this._eventListeners = [
helper.addEventListener(context, BrowserContext.Events.Page, this._onPage.bind(this)),
];
}

onBlob(blob: SnapshotterBlob): void {
Expand Down Expand Up @@ -147,6 +154,7 @@ class ContextTracer implements SnapshotterDelegate {
const event: ActionTraceEvent = {
type: 'action',
contextId: this._contextId,
pageId: this._pageToId.get(metadata.page),
action: metadata.type,
target: await this._targetToString(metadata.target),
value: metadata.value,
Expand All @@ -160,6 +168,30 @@ class ContextTracer implements SnapshotterDelegate {
this._appendTraceEvent(event);
}

private _onPage(page: Page) {
const pageId = 'page@' + createGuid();
this._pageToId.set(page, pageId);

const event: PageCreatedTraceEvent = {
type: 'page-created',
contextId: this._contextId,
pageId,
};
this._appendTraceEvent(event);

page.once(Page.Events.Close, () => {
this._pageToId.delete(page);
if (this._disposed)
return;
const event: PageDestroyedTraceEvent = {
type: 'page-destroyed',
contextId: this._contextId,
pageId,
};
this._appendTraceEvent(event);
});
}

private async _targetToString(target: ElementHandle | string): Promise<string> {
return typeof target === 'string' ? target : await target._previewPromise;
}
Expand All @@ -176,6 +208,9 @@ class ContextTracer implements SnapshotterDelegate {
}

async dispose() {
this._disposed = true;
helper.removeEventListeners(this._eventListeners);
this._pageToId.clear();
this._snapshotter.dispose();
const event: ContextDestroyedTraceEvent = {
type: 'context-destroyed',
Expand Down

0 comments on commit 16be357

Please sign in to comment.