From 16be357489a038a303b811195183d7837116af51 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 11 Sep 2020 11:34:53 -0700 Subject: [PATCH] feat(trace): trace page open/close events (#3852) --- src/trace/traceTypes.ts | 13 +++++ src/trace/traceViewer.ts | 121 ++++++++++++++++++++++----------------- src/trace/tracer.ts | 43 ++++++++++++-- 3 files changed, 121 insertions(+), 56 deletions(-) diff --git a/src/trace/traceTypes.ts b/src/trace/traceTypes.ts index c016cfa67a..5852ba6a01 100644 --- a/src/trace/traceTypes.ts +++ b/src/trace/traceTypes.ts @@ -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, diff --git a/src/trace/traceViewer.ts b/src/trace/traceViewer.ts index 29b0eea20d..d1840d255c 100644 --- a/src/trace/traceViewer.ts +++ b/src/trace/traceViewer.ts @@ -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(); + const pageSections = new Map(); + + 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 { diff --git a/src/trace/tracer.ts b/src/trace/tracer.ts index 7080206aea..a23d2bd457 100644 --- a/src/trace/tracer.ts +++ b/src/trace/tracer.ts @@ -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; private _appendEventChain: Promise; private _writeArtifactChain: Promise; - readonly _snapshotter: Snapshotter; + private _snapshotter: Snapshotter; + private _eventListeners: RegisteredListener[]; + private _disposed = false; + private _pageToId = new Map(); 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 { 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',