feat(trace): trace page open/close events (#3852)
This commit is contained in:
parent
f94df318d5
commit
16be357489
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -26,6 +26,8 @@ const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
|
|||
type TraceEvent =
|
||||
ContextCreatedTraceEvent |
|
||||
ContextDestroyedTraceEvent |
|
||||
PageCreatedTraceEvent |
|
||||
PageDestroyedTraceEvent |
|
||||
NetworkResourceTraceEvent |
|
||||
ActionTraceEvent;
|
||||
|
||||
|
|
@ -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);
|
||||
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.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);
|
||||
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.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 === '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);
|
||||
}
|
||||
}
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -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();
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue