feat(trace): trace page open/close events (#3852)

This commit is contained in:
Dmitry Gozman 2020-09-11 11:34:53 -07:00 committed by GitHub
parent f94df318d5
commit 16be357489
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 121 additions and 56 deletions

View file

@ -38,10 +38,23 @@ export type NetworkResourceTraceEvent = {
sha1: string, 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 = { export type ActionTraceEvent = {
type: 'action', type: 'action',
contextId: string, contextId: string,
action: string, action: string,
pageId?: string,
target?: string, target?: string,
label?: string, label?: string,
value?: string, value?: string,

View file

@ -17,7 +17,7 @@
import * as path from 'path'; import * as path from 'path';
import * as util from 'util'; import * as util from 'util';
import * as fs from 'fs'; 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 { FrameSnapshot, PageSnapshot } from './snapshotter';
import type { Browser, BrowserContext, Frame, Page, Route } from '../client/api'; import type { Browser, BrowserContext, Frame, Page, Route } from '../client/api';
import type { Playwright } from '../client/playwright'; import type { Playwright } from '../client/playwright';
@ -26,6 +26,8 @@ const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
type TraceEvent = type TraceEvent =
ContextCreatedTraceEvent | ContextCreatedTraceEvent |
ContextDestroyedTraceEvent | ContextDestroyedTraceEvent |
PageCreatedTraceEvent |
PageDestroyedTraceEvent |
NetworkResourceTraceEvent | NetworkResourceTraceEvent |
ActionTraceEvent; ActionTraceEvent;
@ -95,62 +97,77 @@ class TraceViewer {
data.actions.push(event); data.actions.push(event);
} }
} }
await uiPage.evaluate(contextData => { await uiPage.evaluate(traces => {
for (const data of Object.values(contextData)) { function createSection(parent: Element, title: string): HTMLDetailsElement {
const header = document.createElement('div'); const details = document.createElement('details');
header.textContent = data.label; details.style.paddingLeft = '10px';
header.style.margin = '10px'; const summary = document.createElement('summary');
document.body.appendChild(header); summary.textContent = title;
for (const action of data.actions) { details.appendChild(summary);
const div = document.createElement('div'); parent.appendChild(details);
div.style.whiteSpace = 'pre'; return details;
div.style.borderBottom = '1px solid black'; }
const lines = [];
lines.push(`action: ${action.action}`); function createField(parent: Element, text: string) {
if (action.label) const div = document.createElement('div');
lines.push(`label: ${action.label}`); div.style.whiteSpace = 'pre';
if (action.target) div.textContent = text;
lines.push(`target: ${action.target}`); parent.appendChild(div);
if (action.value) }
lines.push(`value: ${action.value}`);
if (action.startTime && action.endTime) for (const trace of traces) {
lines.push(`duration: ${action.endTime - action.startTime}ms`); const traceSection = createSection(document.body, trace.traceFile);
div.textContent = lines.join('\n'); traceSection.open = true;
if (action.error) {
const details = document.createElement('details'); const contextSections = new Map<string, Element>();
const summary = document.createElement('summary'); const pageSections = new Map<string, Element>();
summary.textContent = 'error';
details.appendChild(summary); for (const event of trace.events) {
details.appendChild(document.createTextNode(action.error)); if (event.type === 'context-created') {
div.appendChild(details); const contextSection = createSection(traceSection, event.contextId);
contextSection.open = true;
contextSections.set(event.contextId, contextSection);
} }
if (action.stack) { if (event.type === 'page-created') {
const details = document.createElement('details'); const contextSection = contextSections.get(event.contextId)!;
const summary = document.createElement('summary'); const pageSection = createSection(contextSection, event.pageId);
summary.textContent = 'callstack'; pageSection.open = true;
details.appendChild(summary); pageSections.set(event.pageId, pageSection);
details.appendChild(document.createTextNode(action.stack));
div.appendChild(details);
} }
if (action.logs && action.logs.length) { if (event.type === 'action') {
const details = document.createElement('details'); const parentSection = event.pageId ? pageSections.get(event.pageId)! : contextSections.get(event.contextId)!;
const summary = document.createElement('summary'); const actionSection = createSection(parentSection, event.action);
summary.textContent = 'logs'; if (event.label)
details.appendChild(summary); createField(actionSection, `label: ${event.label}`);
details.appendChild(document.createTextNode(action.logs.join('\n'))); if (event.target)
div.appendChild(details); 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);
}
} }
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);
}
document.body.appendChild(div);
} }
} }
}, contextData); }, this._traces);
} }
private async _ensureContext(browser: Browser, contextId: string): Promise<BrowserContext> { private async _ensureContext(browser: Browser, contextId: string): Promise<BrowserContext> {

View file

@ -14,19 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContext } from '../server/browserContext'; import { BrowserContext } from '../server/browserContext';
import type { SanpshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter'; 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 path from 'path';
import * as util from 'util'; import * as util from 'util';
import * as fs from 'fs'; import * as fs from 'fs';
import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../utils/utils'; import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../utils/utils';
import { ActionResult, InstrumentingAgent, instrumentingAgents, ActionMetadata } from '../server/instrumentation'; 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 { Progress, runAbortableTask } from '../server/progress';
import { Snapshotter } from './snapshotter'; import { Snapshotter } from './snapshotter';
import * as types from '../server/types'; import * as types from '../server/types';
import type { ElementHandle } from '../server/dom'; import type { ElementHandle } from '../server/dom';
import { helper, RegisteredListener } from '../server/helper';
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs)); const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
@ -80,7 +81,10 @@ class ContextTracer implements SnapshotterDelegate {
private _traceStoragePromise: Promise<string>; private _traceStoragePromise: Promise<string>;
private _appendEventChain: Promise<string>; private _appendEventChain: Promise<string>;
private _writeArtifactChain: Promise<void>; 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) { constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) {
this._contextId = 'context@' + createGuid(); this._contextId = 'context@' + createGuid();
@ -97,6 +101,9 @@ class ContextTracer implements SnapshotterDelegate {
}; };
this._appendTraceEvent(event); this._appendTraceEvent(event);
this._snapshotter = new Snapshotter(context, this); this._snapshotter = new Snapshotter(context, this);
this._eventListeners = [
helper.addEventListener(context, BrowserContext.Events.Page, this._onPage.bind(this)),
];
} }
onBlob(blob: SnapshotterBlob): void { onBlob(blob: SnapshotterBlob): void {
@ -147,6 +154,7 @@ class ContextTracer implements SnapshotterDelegate {
const event: ActionTraceEvent = { const event: ActionTraceEvent = {
type: 'action', type: 'action',
contextId: this._contextId, contextId: this._contextId,
pageId: this._pageToId.get(metadata.page),
action: metadata.type, action: metadata.type,
target: await this._targetToString(metadata.target), target: await this._targetToString(metadata.target),
value: metadata.value, value: metadata.value,
@ -160,6 +168,30 @@ class ContextTracer implements SnapshotterDelegate {
this._appendTraceEvent(event); 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> { private async _targetToString(target: ElementHandle | string): Promise<string> {
return typeof target === 'string' ? target : await target._previewPromise; return typeof target === 'string' ? target : await target._previewPromise;
} }
@ -176,6 +208,9 @@ class ContextTracer implements SnapshotterDelegate {
} }
async dispose() { async dispose() {
this._disposed = true;
helper.removeEventListeners(this._eventListeners);
this._pageToId.clear();
this._snapshotter.dispose(); this._snapshotter.dispose();
const event: ContextDestroyedTraceEvent = { const event: ContextDestroyedTraceEvent = {
type: 'context-destroyed', type: 'context-destroyed',