feat(trace): show dialogs, navigations and misc events (#5025)

This commit is contained in:
Dmitry Gozman 2021-01-15 18:30:55 -08:00 committed by GitHub
parent e67d563798
commit afaec552dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 288 additions and 100 deletions

View File

@ -15,6 +15,7 @@
*/
import * as trace from '../../trace/traceTypes';
export * as trace from '../../trace/traceTypes';
export type TraceModel = {
contexts: ContextEntry[];
@ -36,11 +37,14 @@ export type VideoEntry = {
videoId: string;
};
export type InterestingPageEvent = trace.DialogOpenedEvent | trace.DialogClosedEvent | trace.NavigationEvent | trace.LoadEvent;
export type PageEntry = {
created: trace.PageCreatedTraceEvent;
destroyed: trace.PageDestroyedTraceEvent;
video?: VideoEntry;
actions: ActionEntry[];
interestingEvents: InterestingPageEvent[];
resources: trace.NetworkResourceTraceEvent[];
}
@ -88,6 +92,7 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel
destroyed: undefined as any,
actions: [],
resources: [],
interestingEvents: [],
};
pageEntries.set(event.pageId, pageEntry);
contextEntries.get(event.contextId)!.pages.push(pageEntry);
@ -129,11 +134,19 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel
responseEvents.push(event);
break;
}
case 'dialog-opened':
case 'dialog-closed':
case 'navigation':
case 'load': {
const pageEntry = pageEntries.get(event.pageId)!;
pageEntry.interestingEvents.push(event);
break;
}
}
const contextEntry = contextEntries.get(event.contextId)!;
contextEntry.startTime = Math.min(contextEntry.startTime, (event as any).timestamp);
contextEntry.endTime = Math.max(contextEntry.endTime, (event as any).timestamp);
contextEntry.startTime = Math.min(contextEntry.startTime, event.timestamp);
contextEntry.endTime = Math.max(contextEntry.endTime, event.timestamp);
}
traceModel.contexts.push(...contextEntries.values());
}

View File

@ -81,7 +81,7 @@ function parseMetaInfo(text: string, video: PageVideoTraceEvent): VideoMetaInfo
width: parseInt(resolutionMatch![1], 10),
height: parseInt(resolutionMatch![2], 10),
fps: parseInt(fpsMatch![1], 10),
startTime: (video as any).timestamp,
endTime: (video as any).timestamp + duration
startTime: video.timestamp,
endTime: video.timestamp + duration
};
}

View File

@ -24,6 +24,7 @@
--purple: #9C27B0;
--yellow: #FFC107;
--blue: #2196F3;
--transparent-blue: #2196F355;
--orange: #d24726;
--black: #1E1E1E;
--gray: #888888;

View File

@ -14,20 +14,19 @@
* limitations under the License.
*/
import { TraceModel, VideoMetaInfo } from '../traceModel';
import { TraceModel, VideoMetaInfo, trace } from '../traceModel';
import './common.css';
import './third_party/vscode/codicon.css';
import { Workbench } from './ui/workbench';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { ActionTraceEvent } from '../../../trace/traceTypes';
declare global {
interface Window {
getTraceModel(): Promise<TraceModel>;
getVideoMetaInfo(videoId: string): Promise<VideoMetaInfo | undefined>;
readFile(filePath: string): Promise<string>;
renderSnapshot(action: ActionTraceEvent): void;
renderSnapshot(action: trace.ActionTraceEvent): void;
}
}

View File

@ -33,7 +33,7 @@
background-color: rgb(0 0 0 / 10%);
}
.timeline-label {
.timeline-time {
position: absolute;
top: 4px;
right: 3px;
@ -58,16 +58,16 @@
left: 0;
}
.timeline-lane.timeline-action-labels {
.timeline-lane.timeline-labels {
margin-top: 10px;
}
.timeline-lane.timeline-actions {
.timeline-lane.timeline-bars {
margin-bottom: 10px;
overflow: visible;
}
.timeline-action {
.timeline-bar {
position: absolute;
top: 0;
bottom: 0;
@ -77,25 +77,37 @@
background-color: var(--action-color);
}
.timeline-action.selected {
.timeline-bar.selected {
filter: brightness(70%);
box-shadow: 0 0 0 1px var(--action-color);
}
.timeline-action.click {
.timeline-bar.click {
--action-color: var(--green);
}
.timeline-action.fill,
.timeline-action.press {
.timeline-bar.fill,
.timeline-bar.press {
--action-color: var(--orange);
}
.timeline-action.goto {
.timeline-bar.goto {
--action-color: var(--blue);
}
.timeline-action-label {
.timeline-bar.dialog {
--action-color: var(--transparent-blue);
}
.timeline-bar.navigation {
--action-color: var(--purple);
}
.timeline-bar.load {
--action-color: var(--yellow);
}
.timeline-label {
position: absolute;
top: 0;
bottom: 0;
@ -103,13 +115,14 @@
background-color: #fffffff0;
justify-content: center;
display: none;
white-space: nowrap;
}
.timeline-action-label.selected {
.timeline-label.selected {
display: flex;
}
.timeline-time-bar {
.timeline-marker {
display: none;
position: absolute;
top: 0;
@ -119,6 +132,6 @@
pointer-events: none;
}
.timeline-time-bar.timeline-time-bar-hover {
.timeline-marker.timeline-marker-hover {
background-color: var(--light-pink);
}

View File

@ -15,13 +15,24 @@
limitations under the License.
*/
import { ContextEntry } from '../../traceModel';
import { ContextEntry, InterestingPageEvent, ActionEntry, trace } from '../../traceModel';
import './timeline.css';
import { FilmStrip } from './filmStrip';
import { Boundaries } from '../geometry';
import * as React from 'react';
import { useMeasure } from './helpers';
import { ActionEntry } from '../../traceModel';
type TimelineBar = {
entry?: ActionEntry;
event?: InterestingPageEvent;
leftPosition: number;
rightPosition: number;
leftTime: number;
rightTime: number;
type: string;
label: string;
priority: number;
};
export const Timeline: React.FunctionComponent<{
context: ContextEntry,
@ -33,51 +44,102 @@ export const Timeline: React.FunctionComponent<{
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
const [measure, ref] = useMeasure<HTMLDivElement>();
const [previewX, setPreviewX] = React.useState<number | undefined>();
const targetAction = highlightedAction || selectedAction;
const [hoveredBar, setHoveredBar] = React.useState<TimelineBar | undefined>();
const offsets = React.useMemo(() => {
return calculateDividerOffsets(measure.width, boundaries);
}, [measure.width, boundaries]);
const actionEntries = React.useMemo(() => {
const actions: ActionEntry[] = [];
for (const page of context.pages)
actions.push(...page.actions);
return actions;
}, [context]);
const actionTimes = React.useMemo(() => {
return actionEntries.map(entry => {
return {
entry,
left: timeToPercent(measure.width, boundaries, entry.action.startTime!),
right: timeToPercent(measure.width, boundaries, entry.action.endTime!),
};
});
}, [actionEntries, boundaries, measure.width]);
const findHoveredAction = (x: number) => {
let targetBar: TimelineBar | undefined = hoveredBar;
const bars = React.useMemo(() => {
const bars: TimelineBar[] = [];
for (const page of context.pages) {
for (const entry of page.actions) {
bars.push({
entry,
leftTime: entry.action.startTime,
rightTime: entry.action.endTime,
leftPosition: timeToPosition(measure.width, boundaries, entry.action.startTime),
rightPosition: timeToPosition(measure.width, boundaries, entry.action.endTime),
label: entry.action.action + ' ' + (entry.action.selector || entry.action.value || ''),
type: entry.action.action,
priority: 0,
});
if (entry === (highlightedAction || selectedAction))
targetBar = bars[bars.length - 1];
}
let lastDialogOpened: trace.DialogOpenedEvent | undefined;
for (const event of page.interestingEvents) {
if (event.type === 'dialog-opened') {
lastDialogOpened = event;
continue;
}
if (event.type === 'dialog-closed' && lastDialogOpened) {
bars.push({
event,
leftTime: lastDialogOpened.timestamp,
rightTime: event.timestamp,
leftPosition: timeToPosition(measure.width, boundaries, lastDialogOpened.timestamp),
rightPosition: timeToPosition(measure.width, boundaries, event.timestamp),
label: lastDialogOpened.message ? `${event.dialogType} "${lastDialogOpened.message}"` : event.dialogType,
type: 'dialog',
priority: -1,
});
} else if (event.type === 'navigation') {
bars.push({
event,
leftTime: event.timestamp,
rightTime: event.timestamp,
leftPosition: timeToPosition(measure.width, boundaries, event.timestamp),
rightPosition: timeToPosition(measure.width, boundaries, event.timestamp),
label: `navigated to ${event.url}`,
type: event.type,
priority: 1,
});
} else if (event.type === 'load') {
bars.push({
event,
leftTime: event.timestamp,
rightTime: event.timestamp,
leftPosition: timeToPosition(measure.width, boundaries, event.timestamp),
rightPosition: timeToPosition(measure.width, boundaries, event.timestamp),
label: `load`,
type: event.type,
priority: 1,
});
}
}
}
bars.sort((a, b) => a.priority - b.priority);
return bars;
}, [context, boundaries, measure.width]);
const findHoveredBar = (x: number) => {
const time = positionToTime(measure.width, boundaries, x);
const time1 = positionToTime(measure.width, boundaries, x - 5);
const time2 = positionToTime(measure.width, boundaries, x + 5);
let entry: ActionEntry | undefined;
let bar: TimelineBar | undefined;
let distance: number | undefined;
for (const e of actionEntries) {
const left = Math.max(e.action.startTime!, time1);
const right = Math.min(e.action.endTime!, time2);
const middle = (e.action.startTime! + e.action.endTime!) / 2;
for (const b of bars) {
const left = Math.max(b.leftTime, time1);
const right = Math.min(b.rightTime, time2);
const middle = (b.leftTime + b.rightTime) / 2;
const d = Math.abs(time - middle);
if (left <= right && (!entry || d < distance!)) {
entry = e;
if (left <= right && (!bar || d < distance!)) {
bar = b;
distance = d;
}
}
return entry;
return bar;
};
const onMouseMove = (event: React.MouseEvent) => {
if (ref.current) {
const x = event.clientX - ref.current.getBoundingClientRect().left;
setPreviewX(x);
onHighlighted(findHoveredAction(x));
const bar = findHoveredBar(x);
setHoveredBar(bar);
onHighlighted(bar && bar.entry ? bar.entry : undefined);
}
};
const onMouseLeave = () => {
@ -86,53 +148,53 @@ export const Timeline: React.FunctionComponent<{
const onClick = (event: React.MouseEvent) => {
if (ref.current) {
const x = event.clientX - ref.current.getBoundingClientRect().left;
const entry = findHoveredAction(x);
if (entry)
onSelected(entry);
const bar = findHoveredBar(x);
if (bar && bar.entry)
onSelected(bar.entry);
}
};
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick}>
<div className='timeline-grid'>{
offsets.map((offset, index) => {
return <div key={index} className='timeline-divider' style={{ left: offset.percent + '%' }}>
<div className='timeline-label'>{msToString(offset.time - boundaries.minimum)}</div>
return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}>
<div className='timeline-time'>{msToString(offset.time - boundaries.minimum)}</div>
</div>;
})
}</div>
<div className='timeline-lane timeline-action-labels'>{
actionTimes.map(({ entry, left, right }) => {
return <div key={entry.actionId}
className={'timeline-action-label ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
<div className='timeline-lane timeline-labels'>{
bars.map((bar, index) => {
return <div key={index}
className={'timeline-label ' + bar.type + (targetBar === bar ? ' selected' : '')}
style={{
left: left + '%',
width: (right - left) + '%',
left: bar.leftPosition + 'px',
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px',
}}
>
{entry.action.action}
{bar.label}
</div>;
})
}</div>
<div className='timeline-lane timeline-actions'>{
actionTimes.map(({ entry, left, right }) => {
return <div key={entry.actionId}
className={'timeline-action ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
<div className='timeline-lane timeline-bars'>{
bars.map((bar, index) => {
return <div key={index}
className={'timeline-bar ' + bar.type + (targetBar === bar ? ' selected' : '')}
style={{
left: left + '%',
width: (right - left) + '%',
left: bar.leftPosition + 'px',
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px',
}}
></div>;
})
}</div>
<FilmStrip context={context} boundaries={boundaries} previewX={previewX} />
<div className='timeline-time-bar timeline-time-bar-hover' style={{
<div className='timeline-marker timeline-marker-hover' style={{
display: (previewX !== undefined) ? 'block' : 'none',
left: (previewX || 0) + 'px',
}}></div>
</div>;
};
function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): { percent: number, time: number }[] {
function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): { position: number, time: number }[] {
const minimumGap = 64;
let dividerCount = clientWidth / minimumGap;
const boundarySpan = boundaries.maximum - boundaries.minimum;
@ -157,19 +219,17 @@ function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): {
const offsets = [];
for (let i = 0; i < dividerCount; ++i) {
const time = firstDividerTime + sectionTime * i;
offsets.push({ percent: timeToPercent(clientWidth, boundaries, time), time });
offsets.push({ position: timeToPosition(clientWidth, boundaries, time), time });
}
return offsets;
}
function timeToPercent(clientWidth: number, boundaries: Boundaries, time: number): number {
const position = (time - boundaries.minimum) / (boundaries.maximum - boundaries.minimum) * clientWidth;
return 100 * position / clientWidth;
function timeToPosition(clientWidth: number, boundaries: Boundaries, time: number): number {
return (time - boundaries.minimum) / (boundaries.maximum - boundaries.minimum) * clientWidth;
}
function positionToTime(clientWidth: number, boundaries: Boundaries, x: number): number {
const percent = x / clientWidth;
return percent * (boundaries.maximum - boundaries.minimum) + boundaries.minimum;
return x / clientWidth * (boundaries.maximum - boundaries.minimum) + boundaries.minimum;
}
function msToString(ms: number): string {

View File

@ -700,6 +700,7 @@ class FrameSession {
_onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) {
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
this._page,
event.type,
event.message,
async (accept: boolean, promptText?: string) => {

View File

@ -16,25 +16,26 @@
*/
import { assert } from '../utils/utils';
import { debugLogger } from '../utils/debugLogger';
import { Page } from './page';
type OnHandle = (accept: boolean, promptText?: string) => Promise<void>;
export type DialogType = 'alert' | 'beforeunload' | 'confirm' | 'prompt';
export class Dialog {
private _page: Page;
private _type: string;
private _message: string;
private _onHandle: OnHandle;
private _handled = false;
private _defaultValue: string;
constructor(type: string, message: string, onHandle: OnHandle, defaultValue?: string) {
constructor(page: Page, type: string, message: string, onHandle: OnHandle, defaultValue?: string) {
this._page = page;
this._type = type;
this._message = message;
this._onHandle = onHandle;
this._defaultValue = defaultValue || '';
debugLogger.log('api', ` ${this._preview()} was shown`);
}
type(): string {
@ -52,22 +53,14 @@ export class Dialog {
async accept(promptText: string | undefined) {
assert(!this._handled, 'Cannot accept dialog which is already handled!');
this._handled = true;
debugLogger.log('api', ` ${this._preview()} was accepted`);
await this._onHandle(true, promptText);
this._page.emit(Page.Events.InternalDialogClosed, this);
}
async dismiss() {
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
this._handled = true;
debugLogger.log('api', ` ${this._preview()} was dismissed`);
await this._onHandle(false);
}
private _preview(): string {
if (!this._message)
return this._type;
if (this._message.length <= 50)
return `${this._type} "${this._message}"`;
return `${this._type} "${this._message.substring(0, 49) + '\u2026'}"`;
this._page.emit(Page.Events.InternalDialogClosed, this);
}
}

View File

@ -219,6 +219,7 @@ export class FFPage implements PageDelegate {
_onDialogOpened(params: Protocol.Page.dialogOpenedPayload) {
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
this._page,
params.type,
params.message,
async (accept: boolean, promptText?: string) => {

View File

@ -98,6 +98,7 @@ export class Page extends EventEmitter {
Crash: 'crash',
Console: 'console',
Dialog: 'dialog',
InternalDialogClosed: 'internaldialogclosed',
Download: 'download',
FileChooser: 'filechooser',
DOMContentLoaded: 'domcontentloaded',

View File

@ -549,6 +549,7 @@ export class WKPage implements PageDelegate {
_onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) {
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
this._page,
event.type as dialog.DialogType,
event.message,
async (accept: boolean, promptText?: string) => {

View File

@ -15,6 +15,7 @@
*/
export type ContextCreatedTraceEvent = {
timestamp: number,
type: 'context-created',
browserName: string,
contextId: string,
@ -24,11 +25,13 @@ export type ContextCreatedTraceEvent = {
};
export type ContextDestroyedTraceEvent = {
timestamp: number,
type: 'context-destroyed',
contextId: string,
};
export type NetworkResourceTraceEvent = {
timestamp: number,
type: 'resource',
contextId: string,
pageId: string,
@ -40,18 +43,21 @@ export type NetworkResourceTraceEvent = {
};
export type PageCreatedTraceEvent = {
timestamp: number,
type: 'page-created',
contextId: string,
pageId: string,
};
export type PageDestroyedTraceEvent = {
timestamp: number,
type: 'page-destroyed',
contextId: string,
pageId: string,
};
export type PageVideoTraceEvent = {
timestamp: number,
type: 'page-video',
contextId: string,
pageId: string,
@ -59,6 +65,7 @@ export type PageVideoTraceEvent = {
};
export type ActionTraceEvent = {
timestamp: number,
type: 'action',
contextId: string,
action: string,
@ -66,8 +73,8 @@ export type ActionTraceEvent = {
selector?: string,
label?: string,
value?: string,
startTime?: number,
endTime?: number,
startTime: number,
endTime: number,
logs?: string[],
snapshot?: {
sha1: string,
@ -77,6 +84,39 @@ export type ActionTraceEvent = {
error?: string,
};
export type DialogOpenedEvent = {
timestamp: number,
type: 'dialog-opened',
contextId: string,
pageId: string,
dialogType: string,
message?: string,
};
export type DialogClosedEvent = {
timestamp: number,
type: 'dialog-closed',
contextId: string,
pageId: string,
dialogType: string,
};
export type NavigationEvent = {
timestamp: number,
type: 'navigation',
contextId: string,
pageId: string,
url: string,
sameDocument: boolean,
};
export type LoadEvent = {
timestamp: number,
type: 'load',
contextId: string,
pageId: string,
};
export type TraceEvent =
ContextCreatedTraceEvent |
ContextDestroyedTraceEvent |
@ -84,7 +124,11 @@ export type TraceEvent =
PageDestroyedTraceEvent |
PageVideoTraceEvent |
NetworkResourceTraceEvent |
ActionTraceEvent;
ActionTraceEvent |
DialogOpenedEvent |
DialogClosedEvent |
NavigationEvent |
LoadEvent;
export type FrameSnapshot = {

View File

@ -16,7 +16,7 @@
import { ActionListener, ActionMetadata, BrowserContext, ContextListener, contextListeners, Video } from '../server/browserContext';
import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent, PageVideoTraceEvent } from './traceTypes';
import * as trace from './traceTypes';
import * as path from 'path';
import * as util from 'util';
import * as fs from 'fs';
@ -27,6 +27,8 @@ import { ElementHandle } from '../server/dom';
import { helper, RegisteredListener } from '../server/helper';
import { DEFAULT_TIMEOUT } from '../utils/timeoutSettings';
import { ProgressResult } from '../server/progress';
import { Dialog } from '../server/dialog';
import { Frame, NavigationEvent } from '../server/frames';
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
@ -86,7 +88,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
this._traceStoragePromise = mkdirIfNeeded(path.join(traceStorageDir, 'sha1')).then(() => traceStorageDir);
this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile);
this._writeArtifactChain = Promise.resolve();
const event: ContextCreatedTraceEvent = {
const event: trace.ContextCreatedTraceEvent = {
timestamp: monotonicTime(),
type: 'context-created',
browserName: context._browser._options.name,
contextId: this._contextId,
@ -107,7 +110,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
}
onResource(resource: SnapshotterResource): void {
const event: NetworkResourceTraceEvent = {
const event: trace.NetworkResourceTraceEvent = {
timestamp: monotonicTime(),
type: 'resource',
contextId: this._contextId,
pageId: resource.pageId,
@ -127,7 +131,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise<void> {
try {
const snapshot = await this._takeSnapshot(metadata.page, typeof metadata.target === 'string' ? undefined : metadata.target);
const event: ActionTraceEvent = {
const event: trace.ActionTraceEvent = {
timestamp: monotonicTime(),
type: 'action',
contextId: this._contextId,
pageId: this._pageToId.get(metadata.page),
@ -150,7 +155,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
const pageId = 'page@' + createGuid();
this._pageToId.set(page, pageId);
const event: PageCreatedTraceEvent = {
const event: trace.PageCreatedTraceEvent = {
timestamp: monotonicTime(),
type: 'page-created',
contextId: this._contextId,
pageId,
@ -160,7 +166,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
page.on(Page.Events.VideoStarted, (video: Video) => {
if (this._disposed)
return;
const event: PageVideoTraceEvent = {
const event: trace.PageVideoTraceEvent = {
timestamp: monotonicTime(),
type: 'page-video',
contextId: this._contextId,
pageId,
@ -169,11 +176,65 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
this._appendTraceEvent(event);
});
page.on(Page.Events.Dialog, (dialog: Dialog) => {
if (this._disposed)
return;
const event: trace.DialogOpenedEvent = {
timestamp: monotonicTime(),
type: 'dialog-opened',
contextId: this._contextId,
pageId,
dialogType: dialog.type(),
message: dialog.message(),
};
this._appendTraceEvent(event);
});
page.on(Page.Events.InternalDialogClosed, (dialog: Dialog) => {
if (this._disposed)
return;
const event: trace.DialogClosedEvent = {
timestamp: monotonicTime(),
type: 'dialog-closed',
contextId: this._contextId,
pageId,
dialogType: dialog.type(),
};
this._appendTraceEvent(event);
});
page.mainFrame().on(Frame.Events.Navigation, (navigationEvent: NavigationEvent) => {
if (this._disposed || page.mainFrame().url() === 'about:blank')
return;
const event: trace.NavigationEvent = {
timestamp: monotonicTime(),
type: 'navigation',
contextId: this._contextId,
pageId,
url: navigationEvent.url,
sameDocument: !navigationEvent.newDocument,
};
this._appendTraceEvent(event);
});
page.on(Page.Events.Load, () => {
if (this._disposed || page.mainFrame().url() === 'about:blank')
return;
const event: trace.LoadEvent = {
timestamp: monotonicTime(),
type: 'load',
contextId: this._contextId,
pageId,
};
this._appendTraceEvent(event);
});
page.once(Page.Events.Close, () => {
this._pageToId.delete(page);
if (this._disposed)
return;
const event: PageDestroyedTraceEvent = {
const event: trace.PageDestroyedTraceEvent = {
timestamp: monotonicTime(),
type: 'page-destroyed',
contextId: this._contextId,
pageId,
@ -204,7 +265,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
helper.removeEventListeners(this._eventListeners);
this._pageToId.clear();
this._snapshotter.dispose();
const event: ContextDestroyedTraceEvent = {
const event: trace.ContextDestroyedTraceEvent = {
timestamp: monotonicTime(),
type: 'context-destroyed',
contextId: this._contextId,
};
@ -234,9 +296,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
private _appendTraceEvent(event: any) {
// Serialize all writes to the trace file.
const timestamp = monotonicTime();
this._appendEventChain = this._appendEventChain.then(async traceFile => {
await fsAppendFileAsync(traceFile, JSON.stringify({...event, timestamp}) + '\n');
await fsAppendFileAsync(traceFile, JSON.stringify(event) + '\n');
return traceFile;
});
}