feat(trace): show dialogs, navigations and misc events (#5025)
This commit is contained in:
parent
e67d563798
commit
afaec552dd
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as trace from '../../trace/traceTypes';
|
import * as trace from '../../trace/traceTypes';
|
||||||
|
export * as trace from '../../trace/traceTypes';
|
||||||
|
|
||||||
export type TraceModel = {
|
export type TraceModel = {
|
||||||
contexts: ContextEntry[];
|
contexts: ContextEntry[];
|
||||||
|
|
@ -36,11 +37,14 @@ export type VideoEntry = {
|
||||||
videoId: string;
|
videoId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InterestingPageEvent = trace.DialogOpenedEvent | trace.DialogClosedEvent | trace.NavigationEvent | trace.LoadEvent;
|
||||||
|
|
||||||
export type PageEntry = {
|
export type PageEntry = {
|
||||||
created: trace.PageCreatedTraceEvent;
|
created: trace.PageCreatedTraceEvent;
|
||||||
destroyed: trace.PageDestroyedTraceEvent;
|
destroyed: trace.PageDestroyedTraceEvent;
|
||||||
video?: VideoEntry;
|
video?: VideoEntry;
|
||||||
actions: ActionEntry[];
|
actions: ActionEntry[];
|
||||||
|
interestingEvents: InterestingPageEvent[];
|
||||||
resources: trace.NetworkResourceTraceEvent[];
|
resources: trace.NetworkResourceTraceEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,6 +92,7 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel
|
||||||
destroyed: undefined as any,
|
destroyed: undefined as any,
|
||||||
actions: [],
|
actions: [],
|
||||||
resources: [],
|
resources: [],
|
||||||
|
interestingEvents: [],
|
||||||
};
|
};
|
||||||
pageEntries.set(event.pageId, pageEntry);
|
pageEntries.set(event.pageId, pageEntry);
|
||||||
contextEntries.get(event.contextId)!.pages.push(pageEntry);
|
contextEntries.get(event.contextId)!.pages.push(pageEntry);
|
||||||
|
|
@ -129,11 +134,19 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel
|
||||||
responseEvents.push(event);
|
responseEvents.push(event);
|
||||||
break;
|
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)!;
|
const contextEntry = contextEntries.get(event.contextId)!;
|
||||||
contextEntry.startTime = Math.min(contextEntry.startTime, (event as any).timestamp);
|
contextEntry.startTime = Math.min(contextEntry.startTime, event.timestamp);
|
||||||
contextEntry.endTime = Math.max(contextEntry.endTime, (event as any).timestamp);
|
contextEntry.endTime = Math.max(contextEntry.endTime, event.timestamp);
|
||||||
}
|
}
|
||||||
traceModel.contexts.push(...contextEntries.values());
|
traceModel.contexts.push(...contextEntries.values());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ function parseMetaInfo(text: string, video: PageVideoTraceEvent): VideoMetaInfo
|
||||||
width: parseInt(resolutionMatch![1], 10),
|
width: parseInt(resolutionMatch![1], 10),
|
||||||
height: parseInt(resolutionMatch![2], 10),
|
height: parseInt(resolutionMatch![2], 10),
|
||||||
fps: parseInt(fpsMatch![1], 10),
|
fps: parseInt(fpsMatch![1], 10),
|
||||||
startTime: (video as any).timestamp,
|
startTime: video.timestamp,
|
||||||
endTime: (video as any).timestamp + duration
|
endTime: video.timestamp + duration
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
--purple: #9C27B0;
|
--purple: #9C27B0;
|
||||||
--yellow: #FFC107;
|
--yellow: #FFC107;
|
||||||
--blue: #2196F3;
|
--blue: #2196F3;
|
||||||
|
--transparent-blue: #2196F355;
|
||||||
--orange: #d24726;
|
--orange: #d24726;
|
||||||
--black: #1E1E1E;
|
--black: #1E1E1E;
|
||||||
--gray: #888888;
|
--gray: #888888;
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,19 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TraceModel, VideoMetaInfo } from '../traceModel';
|
import { TraceModel, VideoMetaInfo, trace } from '../traceModel';
|
||||||
import './common.css';
|
import './common.css';
|
||||||
import './third_party/vscode/codicon.css';
|
import './third_party/vscode/codicon.css';
|
||||||
import { Workbench } from './ui/workbench';
|
import { Workbench } from './ui/workbench';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as ReactDOM from 'react-dom';
|
import * as ReactDOM from 'react-dom';
|
||||||
import { ActionTraceEvent } from '../../../trace/traceTypes';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
getTraceModel(): Promise<TraceModel>;
|
getTraceModel(): Promise<TraceModel>;
|
||||||
getVideoMetaInfo(videoId: string): Promise<VideoMetaInfo | undefined>;
|
getVideoMetaInfo(videoId: string): Promise<VideoMetaInfo | undefined>;
|
||||||
readFile(filePath: string): Promise<string>;
|
readFile(filePath: string): Promise<string>;
|
||||||
renderSnapshot(action: ActionTraceEvent): void;
|
renderSnapshot(action: trace.ActionTraceEvent): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
background-color: rgb(0 0 0 / 10%);
|
background-color: rgb(0 0 0 / 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-label {
|
.timeline-time {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
right: 3px;
|
right: 3px;
|
||||||
|
|
@ -58,16 +58,16 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-lane.timeline-action-labels {
|
.timeline-lane.timeline-labels {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-lane.timeline-actions {
|
.timeline-lane.timeline-bars {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action {
|
.timeline-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
@ -77,25 +77,37 @@
|
||||||
background-color: var(--action-color);
|
background-color: var(--action-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action.selected {
|
.timeline-bar.selected {
|
||||||
filter: brightness(70%);
|
filter: brightness(70%);
|
||||||
box-shadow: 0 0 0 1px var(--action-color);
|
box-shadow: 0 0 0 1px var(--action-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action.click {
|
.timeline-bar.click {
|
||||||
--action-color: var(--green);
|
--action-color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action.fill,
|
.timeline-bar.fill,
|
||||||
.timeline-action.press {
|
.timeline-bar.press {
|
||||||
--action-color: var(--orange);
|
--action-color: var(--orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action.goto {
|
.timeline-bar.goto {
|
||||||
--action-color: var(--blue);
|
--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;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
@ -103,13 +115,14 @@
|
||||||
background-color: #fffffff0;
|
background-color: #fffffff0;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
display: none;
|
display: none;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action-label.selected {
|
.timeline-label.selected {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-time-bar {
|
.timeline-marker {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -119,6 +132,6 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-time-bar.timeline-time-bar-hover {
|
.timeline-marker.timeline-marker-hover {
|
||||||
background-color: var(--light-pink);
|
background-color: var(--light-pink);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,24 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ContextEntry } from '../../traceModel';
|
import { ContextEntry, InterestingPageEvent, ActionEntry, trace } from '../../traceModel';
|
||||||
import './timeline.css';
|
import './timeline.css';
|
||||||
import { FilmStrip } from './filmStrip';
|
import { FilmStrip } from './filmStrip';
|
||||||
import { Boundaries } from '../geometry';
|
import { Boundaries } from '../geometry';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
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<{
|
export const Timeline: React.FunctionComponent<{
|
||||||
context: ContextEntry,
|
context: ContextEntry,
|
||||||
|
|
@ -33,51 +44,102 @@ export const Timeline: React.FunctionComponent<{
|
||||||
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
|
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
|
||||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||||
const [previewX, setPreviewX] = React.useState<number | undefined>();
|
const [previewX, setPreviewX] = React.useState<number | undefined>();
|
||||||
const targetAction = highlightedAction || selectedAction;
|
const [hoveredBar, setHoveredBar] = React.useState<TimelineBar | undefined>();
|
||||||
|
|
||||||
const offsets = React.useMemo(() => {
|
const offsets = React.useMemo(() => {
|
||||||
return calculateDividerOffsets(measure.width, boundaries);
|
return calculateDividerOffsets(measure.width, boundaries);
|
||||||
}, [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 time = positionToTime(measure.width, boundaries, x);
|
||||||
const time1 = positionToTime(measure.width, boundaries, x - 5);
|
const time1 = positionToTime(measure.width, boundaries, x - 5);
|
||||||
const time2 = 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;
|
let distance: number | undefined;
|
||||||
for (const e of actionEntries) {
|
for (const b of bars) {
|
||||||
const left = Math.max(e.action.startTime!, time1);
|
const left = Math.max(b.leftTime, time1);
|
||||||
const right = Math.min(e.action.endTime!, time2);
|
const right = Math.min(b.rightTime, time2);
|
||||||
const middle = (e.action.startTime! + e.action.endTime!) / 2;
|
const middle = (b.leftTime + b.rightTime) / 2;
|
||||||
const d = Math.abs(time - middle);
|
const d = Math.abs(time - middle);
|
||||||
if (left <= right && (!entry || d < distance!)) {
|
if (left <= right && (!bar || d < distance!)) {
|
||||||
entry = e;
|
bar = b;
|
||||||
distance = d;
|
distance = d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return entry;
|
return bar;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseMove = (event: React.MouseEvent) => {
|
const onMouseMove = (event: React.MouseEvent) => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
||||||
setPreviewX(x);
|
setPreviewX(x);
|
||||||
onHighlighted(findHoveredAction(x));
|
const bar = findHoveredBar(x);
|
||||||
|
setHoveredBar(bar);
|
||||||
|
onHighlighted(bar && bar.entry ? bar.entry : undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onMouseLeave = () => {
|
const onMouseLeave = () => {
|
||||||
|
|
@ -86,53 +148,53 @@ export const Timeline: React.FunctionComponent<{
|
||||||
const onClick = (event: React.MouseEvent) => {
|
const onClick = (event: React.MouseEvent) => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
||||||
const entry = findHoveredAction(x);
|
const bar = findHoveredBar(x);
|
||||||
if (entry)
|
if (bar && bar.entry)
|
||||||
onSelected(entry);
|
onSelected(bar.entry);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick}>
|
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick}>
|
||||||
<div className='timeline-grid'>{
|
<div className='timeline-grid'>{
|
||||||
offsets.map((offset, index) => {
|
offsets.map((offset, index) => {
|
||||||
return <div key={index} className='timeline-divider' style={{ left: offset.percent + '%' }}>
|
return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}>
|
||||||
<div className='timeline-label'>{msToString(offset.time - boundaries.minimum)}</div>
|
<div className='timeline-time'>{msToString(offset.time - boundaries.minimum)}</div>
|
||||||
</div>;
|
</div>;
|
||||||
})
|
})
|
||||||
}</div>
|
}</div>
|
||||||
<div className='timeline-lane timeline-action-labels'>{
|
<div className='timeline-lane timeline-labels'>{
|
||||||
actionTimes.map(({ entry, left, right }) => {
|
bars.map((bar, index) => {
|
||||||
return <div key={entry.actionId}
|
return <div key={index}
|
||||||
className={'timeline-action-label ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
|
className={'timeline-label ' + bar.type + (targetBar === bar ? ' selected' : '')}
|
||||||
style={{
|
style={{
|
||||||
left: left + '%',
|
left: bar.leftPosition + 'px',
|
||||||
width: (right - left) + '%',
|
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{entry.action.action}
|
{bar.label}
|
||||||
</div>;
|
</div>;
|
||||||
})
|
})
|
||||||
}</div>
|
}</div>
|
||||||
<div className='timeline-lane timeline-actions'>{
|
<div className='timeline-lane timeline-bars'>{
|
||||||
actionTimes.map(({ entry, left, right }) => {
|
bars.map((bar, index) => {
|
||||||
return <div key={entry.actionId}
|
return <div key={index}
|
||||||
className={'timeline-action ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
|
className={'timeline-bar ' + bar.type + (targetBar === bar ? ' selected' : '')}
|
||||||
style={{
|
style={{
|
||||||
left: left + '%',
|
left: bar.leftPosition + 'px',
|
||||||
width: (right - left) + '%',
|
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px',
|
||||||
}}
|
}}
|
||||||
></div>;
|
></div>;
|
||||||
})
|
})
|
||||||
}</div>
|
}</div>
|
||||||
<FilmStrip context={context} boundaries={boundaries} previewX={previewX} />
|
<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',
|
display: (previewX !== undefined) ? 'block' : 'none',
|
||||||
left: (previewX || 0) + 'px',
|
left: (previewX || 0) + 'px',
|
||||||
}}></div>
|
}}></div>
|
||||||
</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;
|
const minimumGap = 64;
|
||||||
let dividerCount = clientWidth / minimumGap;
|
let dividerCount = clientWidth / minimumGap;
|
||||||
const boundarySpan = boundaries.maximum - boundaries.minimum;
|
const boundarySpan = boundaries.maximum - boundaries.minimum;
|
||||||
|
|
@ -157,19 +219,17 @@ function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): {
|
||||||
const offsets = [];
|
const offsets = [];
|
||||||
for (let i = 0; i < dividerCount; ++i) {
|
for (let i = 0; i < dividerCount; ++i) {
|
||||||
const time = firstDividerTime + sectionTime * i;
|
const time = firstDividerTime + sectionTime * i;
|
||||||
offsets.push({ percent: timeToPercent(clientWidth, boundaries, time), time });
|
offsets.push({ position: timeToPosition(clientWidth, boundaries, time), time });
|
||||||
}
|
}
|
||||||
return offsets;
|
return offsets;
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeToPercent(clientWidth: number, boundaries: Boundaries, time: number): number {
|
function timeToPosition(clientWidth: number, boundaries: Boundaries, time: number): number {
|
||||||
const position = (time - boundaries.minimum) / (boundaries.maximum - boundaries.minimum) * clientWidth;
|
return (time - boundaries.minimum) / (boundaries.maximum - boundaries.minimum) * clientWidth;
|
||||||
return 100 * position / clientWidth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function positionToTime(clientWidth: number, boundaries: Boundaries, x: number): number {
|
function positionToTime(clientWidth: number, boundaries: Boundaries, x: number): number {
|
||||||
const percent = x / clientWidth;
|
return x / clientWidth * (boundaries.maximum - boundaries.minimum) + boundaries.minimum;
|
||||||
return percent * (boundaries.maximum - boundaries.minimum) + boundaries.minimum;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function msToString(ms: number): string {
|
function msToString(ms: number): string {
|
||||||
|
|
|
||||||
|
|
@ -700,6 +700,7 @@ class FrameSession {
|
||||||
|
|
||||||
_onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) {
|
_onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) {
|
||||||
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
|
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
|
||||||
|
this._page,
|
||||||
event.type,
|
event.type,
|
||||||
event.message,
|
event.message,
|
||||||
async (accept: boolean, promptText?: string) => {
|
async (accept: boolean, promptText?: string) => {
|
||||||
|
|
|
||||||
|
|
@ -16,25 +16,26 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert } from '../utils/utils';
|
import { assert } from '../utils/utils';
|
||||||
import { debugLogger } from '../utils/debugLogger';
|
import { Page } from './page';
|
||||||
|
|
||||||
type OnHandle = (accept: boolean, promptText?: string) => Promise<void>;
|
type OnHandle = (accept: boolean, promptText?: string) => Promise<void>;
|
||||||
|
|
||||||
export type DialogType = 'alert' | 'beforeunload' | 'confirm' | 'prompt';
|
export type DialogType = 'alert' | 'beforeunload' | 'confirm' | 'prompt';
|
||||||
|
|
||||||
export class Dialog {
|
export class Dialog {
|
||||||
|
private _page: Page;
|
||||||
private _type: string;
|
private _type: string;
|
||||||
private _message: string;
|
private _message: string;
|
||||||
private _onHandle: OnHandle;
|
private _onHandle: OnHandle;
|
||||||
private _handled = false;
|
private _handled = false;
|
||||||
private _defaultValue: string;
|
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._type = type;
|
||||||
this._message = message;
|
this._message = message;
|
||||||
this._onHandle = onHandle;
|
this._onHandle = onHandle;
|
||||||
this._defaultValue = defaultValue || '';
|
this._defaultValue = defaultValue || '';
|
||||||
debugLogger.log('api', ` ${this._preview()} was shown`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type(): string {
|
type(): string {
|
||||||
|
|
@ -52,22 +53,14 @@ export class Dialog {
|
||||||
async accept(promptText: string | undefined) {
|
async accept(promptText: string | undefined) {
|
||||||
assert(!this._handled, 'Cannot accept dialog which is already handled!');
|
assert(!this._handled, 'Cannot accept dialog which is already handled!');
|
||||||
this._handled = true;
|
this._handled = true;
|
||||||
debugLogger.log('api', ` ${this._preview()} was accepted`);
|
|
||||||
await this._onHandle(true, promptText);
|
await this._onHandle(true, promptText);
|
||||||
|
this._page.emit(Page.Events.InternalDialogClosed, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async dismiss() {
|
async dismiss() {
|
||||||
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
|
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
|
||||||
this._handled = true;
|
this._handled = true;
|
||||||
debugLogger.log('api', ` ${this._preview()} was dismissed`);
|
|
||||||
await this._onHandle(false);
|
await this._onHandle(false);
|
||||||
}
|
this._page.emit(Page.Events.InternalDialogClosed, this);
|
||||||
|
|
||||||
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'}"`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@ export class FFPage implements PageDelegate {
|
||||||
|
|
||||||
_onDialogOpened(params: Protocol.Page.dialogOpenedPayload) {
|
_onDialogOpened(params: Protocol.Page.dialogOpenedPayload) {
|
||||||
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
|
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
|
||||||
|
this._page,
|
||||||
params.type,
|
params.type,
|
||||||
params.message,
|
params.message,
|
||||||
async (accept: boolean, promptText?: string) => {
|
async (accept: boolean, promptText?: string) => {
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ export class Page extends EventEmitter {
|
||||||
Crash: 'crash',
|
Crash: 'crash',
|
||||||
Console: 'console',
|
Console: 'console',
|
||||||
Dialog: 'dialog',
|
Dialog: 'dialog',
|
||||||
|
InternalDialogClosed: 'internaldialogclosed',
|
||||||
Download: 'download',
|
Download: 'download',
|
||||||
FileChooser: 'filechooser',
|
FileChooser: 'filechooser',
|
||||||
DOMContentLoaded: 'domcontentloaded',
|
DOMContentLoaded: 'domcontentloaded',
|
||||||
|
|
|
||||||
|
|
@ -549,6 +549,7 @@ export class WKPage implements PageDelegate {
|
||||||
|
|
||||||
_onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) {
|
_onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) {
|
||||||
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
|
this._page.emit(Page.Events.Dialog, new dialog.Dialog(
|
||||||
|
this._page,
|
||||||
event.type as dialog.DialogType,
|
event.type as dialog.DialogType,
|
||||||
event.message,
|
event.message,
|
||||||
async (accept: boolean, promptText?: string) => {
|
async (accept: boolean, promptText?: string) => {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ContextCreatedTraceEvent = {
|
export type ContextCreatedTraceEvent = {
|
||||||
|
timestamp: number,
|
||||||
type: 'context-created',
|
type: 'context-created',
|
||||||
browserName: string,
|
browserName: string,
|
||||||
contextId: string,
|
contextId: string,
|
||||||
|
|
@ -24,11 +25,13 @@ export type ContextCreatedTraceEvent = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextDestroyedTraceEvent = {
|
export type ContextDestroyedTraceEvent = {
|
||||||
|
timestamp: number,
|
||||||
type: 'context-destroyed',
|
type: 'context-destroyed',
|
||||||
contextId: string,
|
contextId: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NetworkResourceTraceEvent = {
|
export type NetworkResourceTraceEvent = {
|
||||||
|
timestamp: number,
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
contextId: string,
|
contextId: string,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
|
|
@ -40,18 +43,21 @@ export type NetworkResourceTraceEvent = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PageCreatedTraceEvent = {
|
export type PageCreatedTraceEvent = {
|
||||||
|
timestamp: number,
|
||||||
type: 'page-created',
|
type: 'page-created',
|
||||||
contextId: string,
|
contextId: string,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PageDestroyedTraceEvent = {
|
export type PageDestroyedTraceEvent = {
|
||||||
|
timestamp: number,
|
||||||
type: 'page-destroyed',
|
type: 'page-destroyed',
|
||||||
contextId: string,
|
contextId: string,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PageVideoTraceEvent = {
|
export type PageVideoTraceEvent = {
|
||||||
|
timestamp: number,
|
||||||
type: 'page-video',
|
type: 'page-video',
|
||||||
contextId: string,
|
contextId: string,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
|
|
@ -59,6 +65,7 @@ export type PageVideoTraceEvent = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActionTraceEvent = {
|
export type ActionTraceEvent = {
|
||||||
|
timestamp: number,
|
||||||
type: 'action',
|
type: 'action',
|
||||||
contextId: string,
|
contextId: string,
|
||||||
action: string,
|
action: string,
|
||||||
|
|
@ -66,8 +73,8 @@ export type ActionTraceEvent = {
|
||||||
selector?: string,
|
selector?: string,
|
||||||
label?: string,
|
label?: string,
|
||||||
value?: string,
|
value?: string,
|
||||||
startTime?: number,
|
startTime: number,
|
||||||
endTime?: number,
|
endTime: number,
|
||||||
logs?: string[],
|
logs?: string[],
|
||||||
snapshot?: {
|
snapshot?: {
|
||||||
sha1: string,
|
sha1: string,
|
||||||
|
|
@ -77,6 +84,39 @@ export type ActionTraceEvent = {
|
||||||
error?: string,
|
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 =
|
export type TraceEvent =
|
||||||
ContextCreatedTraceEvent |
|
ContextCreatedTraceEvent |
|
||||||
ContextDestroyedTraceEvent |
|
ContextDestroyedTraceEvent |
|
||||||
|
|
@ -84,7 +124,11 @@ export type TraceEvent =
|
||||||
PageDestroyedTraceEvent |
|
PageDestroyedTraceEvent |
|
||||||
PageVideoTraceEvent |
|
PageVideoTraceEvent |
|
||||||
NetworkResourceTraceEvent |
|
NetworkResourceTraceEvent |
|
||||||
ActionTraceEvent;
|
ActionTraceEvent |
|
||||||
|
DialogOpenedEvent |
|
||||||
|
DialogClosedEvent |
|
||||||
|
NavigationEvent |
|
||||||
|
LoadEvent;
|
||||||
|
|
||||||
|
|
||||||
export type FrameSnapshot = {
|
export type FrameSnapshot = {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import { ActionListener, ActionMetadata, BrowserContext, ContextListener, contextListeners, Video } from '../server/browserContext';
|
import { ActionListener, ActionMetadata, BrowserContext, ContextListener, contextListeners, Video } from '../server/browserContext';
|
||||||
import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
|
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 path from 'path';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
|
@ -27,6 +27,8 @@ import { ElementHandle } from '../server/dom';
|
||||||
import { helper, RegisteredListener } from '../server/helper';
|
import { helper, RegisteredListener } from '../server/helper';
|
||||||
import { DEFAULT_TIMEOUT } from '../utils/timeoutSettings';
|
import { DEFAULT_TIMEOUT } from '../utils/timeoutSettings';
|
||||||
import { ProgressResult } from '../server/progress';
|
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 fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
|
||||||
const fsAppendFileAsync = util.promisify(fs.appendFile.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._traceStoragePromise = mkdirIfNeeded(path.join(traceStorageDir, 'sha1')).then(() => traceStorageDir);
|
||||||
this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile);
|
this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile);
|
||||||
this._writeArtifactChain = Promise.resolve();
|
this._writeArtifactChain = Promise.resolve();
|
||||||
const event: ContextCreatedTraceEvent = {
|
const event: trace.ContextCreatedTraceEvent = {
|
||||||
|
timestamp: monotonicTime(),
|
||||||
type: 'context-created',
|
type: 'context-created',
|
||||||
browserName: context._browser._options.name,
|
browserName: context._browser._options.name,
|
||||||
contextId: this._contextId,
|
contextId: this._contextId,
|
||||||
|
|
@ -107,7 +110,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
onResource(resource: SnapshotterResource): void {
|
onResource(resource: SnapshotterResource): void {
|
||||||
const event: NetworkResourceTraceEvent = {
|
const event: trace.NetworkResourceTraceEvent = {
|
||||||
|
timestamp: monotonicTime(),
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
contextId: this._contextId,
|
contextId: this._contextId,
|
||||||
pageId: resource.pageId,
|
pageId: resource.pageId,
|
||||||
|
|
@ -127,7 +131,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
|
||||||
async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise<void> {
|
async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const snapshot = await this._takeSnapshot(metadata.page, typeof metadata.target === 'string' ? undefined : metadata.target);
|
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',
|
type: 'action',
|
||||||
contextId: this._contextId,
|
contextId: this._contextId,
|
||||||
pageId: this._pageToId.get(metadata.page),
|
pageId: this._pageToId.get(metadata.page),
|
||||||
|
|
@ -150,7 +155,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
|
||||||
const pageId = 'page@' + createGuid();
|
const pageId = 'page@' + createGuid();
|
||||||
this._pageToId.set(page, pageId);
|
this._pageToId.set(page, pageId);
|
||||||
|
|
||||||
const event: PageCreatedTraceEvent = {
|
const event: trace.PageCreatedTraceEvent = {
|
||||||
|
timestamp: monotonicTime(),
|
||||||
type: 'page-created',
|
type: 'page-created',
|
||||||
contextId: this._contextId,
|
contextId: this._contextId,
|
||||||
pageId,
|
pageId,
|
||||||
|
|
@ -160,7 +166,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
|
||||||
page.on(Page.Events.VideoStarted, (video: Video) => {
|
page.on(Page.Events.VideoStarted, (video: Video) => {
|
||||||
if (this._disposed)
|
if (this._disposed)
|
||||||
return;
|
return;
|
||||||
const event: PageVideoTraceEvent = {
|
const event: trace.PageVideoTraceEvent = {
|
||||||
|
timestamp: monotonicTime(),
|
||||||
type: 'page-video',
|
type: 'page-video',
|
||||||
contextId: this._contextId,
|
contextId: this._contextId,
|
||||||
pageId,
|
pageId,
|
||||||
|
|
@ -169,11 +176,65 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
|
||||||
this._appendTraceEvent(event);
|
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, () => {
|
page.once(Page.Events.Close, () => {
|
||||||
this._pageToId.delete(page);
|
this._pageToId.delete(page);
|
||||||
if (this._disposed)
|
if (this._disposed)
|
||||||
return;
|
return;
|
||||||
const event: PageDestroyedTraceEvent = {
|
const event: trace.PageDestroyedTraceEvent = {
|
||||||
|
timestamp: monotonicTime(),
|
||||||
type: 'page-destroyed',
|
type: 'page-destroyed',
|
||||||
contextId: this._contextId,
|
contextId: this._contextId,
|
||||||
pageId,
|
pageId,
|
||||||
|
|
@ -204,7 +265,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
|
||||||
helper.removeEventListeners(this._eventListeners);
|
helper.removeEventListeners(this._eventListeners);
|
||||||
this._pageToId.clear();
|
this._pageToId.clear();
|
||||||
this._snapshotter.dispose();
|
this._snapshotter.dispose();
|
||||||
const event: ContextDestroyedTraceEvent = {
|
const event: trace.ContextDestroyedTraceEvent = {
|
||||||
|
timestamp: monotonicTime(),
|
||||||
type: 'context-destroyed',
|
type: 'context-destroyed',
|
||||||
contextId: this._contextId,
|
contextId: this._contextId,
|
||||||
};
|
};
|
||||||
|
|
@ -234,9 +296,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
|
||||||
|
|
||||||
private _appendTraceEvent(event: any) {
|
private _appendTraceEvent(event: any) {
|
||||||
// Serialize all writes to the trace file.
|
// Serialize all writes to the trace file.
|
||||||
const timestamp = monotonicTime();
|
|
||||||
this._appendEventChain = this._appendEventChain.then(async traceFile => {
|
this._appendEventChain = this._appendEventChain.then(async traceFile => {
|
||||||
await fsAppendFileAsync(traceFile, JSON.stringify({...event, timestamp}) + '\n');
|
await fsAppendFileAsync(traceFile, JSON.stringify(event) + '\n');
|
||||||
return traceFile;
|
return traceFile;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue