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,
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue