chore: flatten metadata in trace events (#21214)

This commit is contained in:
Pavel Feldman 2023-02-27 15:29:20 -08:00 committed by GitHub
parent e17e0e40f8
commit 22d82b6e1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 406 additions and 146 deletions

View File

@ -26,6 +26,7 @@ import { rewriteErrorMessage } from '../../utils/stackTrace';
import type { PlaywrightDispatcher } from './playwrightDispatcher';
import { eventsHelper } from '../..//utils/eventsHelper';
import type { RegisteredListener } from '../..//utils/eventsHelper';
import type * as trace from '@trace/trace';
export const dispatcherSymbol = Symbol('dispatcher');
const metadataValidator = createMetadataValidator();
@ -186,20 +187,15 @@ export class DispatcherConnection {
private _sendMessageToClient(guid: string, type: string, method: string, params: any, sdkObject?: SdkObject) {
if (sdkObject) {
const eventMetadata: CallMetadata = {
id: `event@${++lastEventId}`,
objectId: sdkObject?.guid,
pageId: sdkObject?.attribution?.page?.guid,
frameId: sdkObject?.attribution?.frame?.guid,
startTime: monotonicTime(),
endTime: 0,
type,
const event: trace.EventTraceEvent = {
type: 'event',
class: type,
method,
params: params || {},
log: [],
snapshots: []
time: monotonicTime(),
pageId: sdkObject?.attribution?.page?.guid,
};
sdkObject.instrumentation?.onEvent(sdkObject, eventMetadata);
sdkObject.instrumentation?.onEvent(sdkObject, event);
}
this.onmessage({ guid, method, params });
}
@ -330,5 +326,3 @@ function formatLogRecording(log: string[]): string {
const rightLength = headerLength - header.length - leftLength;
return `\n${'='.repeat(leftLength)}${header}${'='.repeat(rightLength)}\n${log.join('\n')}\n${'='.repeat(headerLength)}`;
}
let lastEventId = 0;

View File

@ -35,6 +35,7 @@ export type Attribution = {
import type { CallMetadata } from '@protocol/callMetadata';
export type { CallMetadata } from '@protocol/callMetadata';
import type * as trace from '@trace/trace';
export const kTestSdkObjects = new WeakSet<SdkObject>();
@ -61,7 +62,7 @@ export interface Instrumentation {
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onEvent(sdkObject: SdkObject, metadata: CallMetadata): void;
onEvent(sdkObject: SdkObject, event: trace.EventTraceEvent): void;
onPageOpen(page: Page): void;
onPageClose(page: Page): void;
onBrowserOpen(browser: Browser): void;
@ -73,7 +74,7 @@ export interface InstrumentationListener {
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void;
onEvent?(sdkObject: SdkObject, event: trace.EventTraceEvent): void;
onPageOpen?(page: Page): void;
onPageClose?(page: Page): void;
onBrowserOpen?(browser: Browser): void;

View File

@ -43,7 +43,7 @@ import type { SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
import { Snapshotter } from './snapshotter';
import { yazl } from '../../../zipBundle';
const version: VERSION = 3;
const version: VERSION = 4;
export type TracerOptions = {
name?: string;
@ -341,15 +341,25 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
}
pendingCall.afterSnapshot = this._captureSnapshot('after', sdkObject, metadata);
await pendingCall.afterSnapshot;
const event: trace.ActionTraceEvent = { type: 'action', metadata };
this._appendTraceEvent(event);
const event = createActionTraceEvent(metadata);
if (event)
this._appendTraceEvent(event);
this._pendingCalls.delete(metadata.id);
}
onEvent(sdkObject: SdkObject, metadata: CallMetadata) {
onEvent(sdkObject: SdkObject, event: trace.EventTraceEvent) {
if (!sdkObject.attribution.context)
return;
const event: trace.ActionTraceEvent = { type: 'event', metadata };
if (event.method === '__create__' && event.class === 'ConsoleMessage') {
const object: trace.ObjectTraceEvent = {
type: 'object',
class: event.class,
guid: event.params.guid,
initializer: event.params.initializer,
};
this._appendTraceEvent(object);
return;
}
this._appendTraceEvent(event);
}
@ -467,3 +477,25 @@ function visitTraceEvent(object: any, sha1s: Set<string>): any {
export function shouldCaptureSnapshot(metadata: CallMetadata): boolean {
return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method);
}
function createActionTraceEvent(metadata: CallMetadata): trace.ActionTraceEvent | null {
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
return {
type: 'action',
callId: metadata.id,
startTime: metadata.startTime,
endTime: metadata.endTime,
apiName: metadata.apiName || metadata.type + '.' + metadata.method,
class: metadata.type,
method: metadata.method,
params: metadata.params,
wallTime: metadata.wallTime || Date.now(),
log: metadata.log,
snapshots: metadata.snapshots,
error: metadata.error?.error,
result: metadata.result,
point: metadata.point,
pageId: metadata.pageId,
};
}

View File

@ -32,8 +32,8 @@ export type ContextEntry = {
pages: PageEntry[];
resources: ResourceSnapshot[];
actions: trace.ActionTraceEvent[];
events: trace.ActionTraceEvent[];
objects: { [key: string]: any };
events: trace.EventTraceEvent[];
initializers: { [key: string]: any };
hasSource: boolean;
};
@ -60,7 +60,7 @@ export function createEmptyContext(): ContextEntry {
resources: [],
actions: [],
events: [],
objects: {},
initializers: {},
hasSource: false
};
}

View File

@ -14,8 +14,8 @@
* limitations under the License.
*/
import type { CallMetadata } from '@protocol/callMetadata';
import type * as trace from '@trace/trace';
import type * as traceV3 from './versions/traceV3';
import { parseClientSideCallMetadata } from '@trace/traceUtils';
import type zip from '@zip.js/zip.js';
// @ts-ignore
@ -87,7 +87,7 @@ export class TraceModel {
await stacksEntry.getData!(writer);
const metadataMap = parseClientSideCallMetadata(JSON.parse(await writer.getData()));
for (const action of this.contextEntry.actions)
action.metadata.stack = action.metadata.stack || metadataMap.get(action.metadata.id);
action.stack = action.stack || metadataMap.get(action.callId);
}
this._build();
@ -117,7 +117,7 @@ export class TraceModel {
}
private _build() {
this.contextEntry!.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
this.contextEntry!.actions.sort((a1, a2) => a1.startTime - a2.startTime);
this.contextEntry!.resources = this._snapshotStorage!.resources();
}
@ -137,6 +137,8 @@ export class TraceModel {
if (!line)
return;
const event = this._modernize(JSON.parse(line));
if (!event)
return;
switch (event.type) {
case 'context-options': {
this.contextEntry.browserName = event.browserName;
@ -153,22 +155,15 @@ export class TraceModel {
break;
}
case 'action': {
const include = !isTracing(event.metadata) && (!event.metadata.internal || event.metadata.apiName);
if (include) {
if (!event.metadata.apiName)
event.metadata.apiName = event.metadata.type + '.' + event.metadata.method;
this.contextEntry!.actions.push(event);
}
this.contextEntry!.actions.push(event);
break;
}
case 'event': {
const metadata = event.metadata;
if (metadata.pageId) {
if (metadata.method === '__create__')
this.contextEntry!.objects[metadata.params.guid] = metadata.params.initializer;
else
this.contextEntry!.events.push(event);
}
this.contextEntry!.events.push(event);
break;
}
case 'object': {
this.contextEntry!.initializers[event.guid] = event.initializer;
break;
}
case 'resource-snapshot':
@ -178,9 +173,13 @@ export class TraceModel {
this._snapshotStorage!.addFrameSnapshot(event.snapshot);
break;
}
if (event.type === 'action' || event.type === 'event') {
this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.metadata.startTime);
this.contextEntry!.endTime = Math.max(this.contextEntry!.endTime, event.metadata.endTime);
if (event.type === 'action') {
this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.startTime);
this.contextEntry!.endTime = Math.max(this.contextEntry!.endTime, event.endTime);
}
if (event.type === 'event') {
this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.time);
this.contextEntry!.endTime = Math.max(this.contextEntry!.endTime, event.time);
}
if (event.type === 'screencast-frame') {
this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.timestamp);
@ -237,6 +236,54 @@ export class TraceModel {
}
return event;
}
_modernize_3_to_4(event: traceV3.TraceEvent): trace.TraceEvent | null {
if (event.type !== 'action') {
return event as traceV3.ContextCreatedTraceEvent |
traceV3.ScreencastFrameTraceEvent |
traceV3.ResourceSnapshotTraceEvent |
traceV3.FrameSnapshotTraceEvent;
}
const metadata = event.metadata;
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
if (metadata.id.startsWith('event@')) {
if (metadata.method === '__create__' && metadata.type === 'ConsoleMessage') {
return {
type: 'object',
class: metadata.type,
guid: metadata.params.guid,
initializer: metadata.params.initializer,
};
}
return {
type: 'event',
time: metadata.startTime,
class: metadata.type,
method: metadata.method,
params: metadata.params,
pageId: metadata.pageId,
};
}
return {
type: 'action',
callId: metadata.id,
startTime: metadata.startTime,
endTime: metadata.endTime,
apiName: metadata.apiName || metadata.type + '.' + metadata.method,
class: metadata.type,
method: metadata.method,
params: metadata.params,
wallTime: metadata.wallTime || Date.now(),
log: metadata.log,
snapshots: metadata.snapshots,
error: metadata.error?.error,
result: metadata.result,
point: metadata.point,
pageId: metadata.pageId,
};
}
}
export class PersistentSnapshotStorage extends BaseSnapshotStorage {
@ -254,7 +301,3 @@ export class PersistentSnapshotStorage extends BaseSnapshotStorage {
return writer.getData();
}
}
function isTracing(metadata: CallMetadata): boolean {
return metadata.method.startsWith('tracing');
}

View File

@ -45,8 +45,8 @@ export const ActionList: React.FC<ActionListProps> = ({
selectedItem={selectedAction}
onSelected={(action: ActionTraceEvent) => onSelected(action)}
onHighlighted={(action: ActionTraceEvent) => onHighlighted(action)}
itemKey={(action: ActionTraceEvent) => action.metadata.id}
itemType={(action: ActionTraceEvent) => action.metadata.error?.error?.message ? 'error' : undefined}
itemKey={(action: ActionTraceEvent) => action.callId}
itemType={(action: ActionTraceEvent) => action.error?.message ? 'error' : undefined}
itemRender={(action: ActionTraceEvent) => renderAction(action, sdkLanguage, setSelectedTab)}
showNoItemsMessage={true}
></ListView>;
@ -57,17 +57,16 @@ const renderAction = (
sdkLanguage: Language | undefined,
setSelectedTab: (tab: string) => void
) => {
const { metadata } = action;
const { errors, warnings } = modelUtil.stats(action);
const locator = metadata.params.selector ? asLocator(sdkLanguage || 'javascript', metadata.params.selector) : undefined;
const locator = action.params.selector ? asLocator(sdkLanguage || 'javascript', action.params.selector) : undefined;
return <>
<div className='action-title'>
<span>{metadata.apiName}</span>
<span>{action.apiName}</span>
{locator && <div className='action-selector' title={locator}>{locator}</div>}
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
</div>
<div className='action-duration' style={{ flex: 'none' }}>{metadata.endTime ? msToString(metadata.endTime - metadata.startTime) : 'Timed Out'}</div>
<div className='action-duration' style={{ flex: 'none' }}>{action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out'}</div>
<div className='action-icons' onClick={() => setSelectedTab('console')}>
{!!errors && <div className='action-icon'><span className={'codicon codicon-error'}></span><span className="action-icon-value">{errors}</span></div>}
{!!warnings && <div className='action-icon'><span className={'codicon codicon-warning'}></span><span className="action-icon-value">{warnings}</span></div>}

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import type { CallMetadata } from '@protocol/callMetadata';
import type { SerializedValue } from '@protocol/channels';
import type { ActionTraceEvent } from '@trace/trace';
import { msToString } from '@web/uiUtils';
@ -30,20 +29,20 @@ export const CallTab: React.FunctionComponent<{
}> = ({ action, sdkLanguage }) => {
if (!action)
return null;
const logs = action.metadata.log;
const error = action.metadata.error?.error?.message;
const params = { ...action.metadata.params };
const logs = action.log;
const error = action.error?.message;
const params = { ...action.params };
// Strip down the waitForEventInfo data, we never need it.
delete params.info;
const paramKeys = Object.keys(params);
const wallTime = action.metadata.wallTime ? new Date(action.metadata.wallTime).toLocaleString() : null;
const duration = action.metadata.endTime ? msToString(action.metadata.endTime - action.metadata.startTime) : 'Timed Out';
const wallTime = action.wallTime ? new Date(action.wallTime).toLocaleString() : null;
const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out';
return <div className='call-tab'>
<div className='call-error' key='error' hidden={!error}>
<div className='codicon codicon-issues'/>
{error}
</div>
<div className='call-line'>{action.metadata.apiName}</div>
<div className='call-line'>{action.apiName}</div>
{<>
<div className='call-section'>Time</div>
{wallTime && <div className='call-line'>wall time:<span className='call-value datetime' title={wallTime}>{wallTime}</span></div>}
@ -51,12 +50,12 @@ export const CallTab: React.FunctionComponent<{
</>}
{ !!paramKeys.length && <div className='call-section'>Parameters</div> }
{
!!paramKeys.length && paramKeys.map((name, index) => renderProperty(propertyToString(action.metadata, name, params[name], sdkLanguage), 'param-' + index))
!!paramKeys.length && paramKeys.map((name, index) => renderProperty(propertyToString(action, name, params[name], sdkLanguage), 'param-' + index))
}
{ !!action.metadata.result && <div className='call-section'>Return value</div> }
{ !!action.result && <div className='call-section'>Return value</div> }
{
!!action.metadata.result && Object.keys(action.metadata.result).map((name, index) =>
renderProperty(propertyToString(action.metadata, name, action.metadata.result[name], sdkLanguage), 'result-' + index)
!!action.result && Object.keys(action.result).map((name, index) =>
renderProperty(propertyToString(action, name, action.result[name], sdkLanguage), 'result-' + index)
)
}
<div className='call-section'>Log</div>
@ -90,14 +89,14 @@ function renderProperty(property: Property, key: string) {
);
}
function propertyToString(metadata: CallMetadata, name: string, value: any, sdkLanguage: Language | undefined): Property {
const isEval = metadata.method.includes('eval') || metadata.method === 'waitForFunction';
function propertyToString(event: ActionTraceEvent, name: string, value: any, sdkLanguage: Language | undefined): Property {
const isEval = event.method.includes('eval') || event.method === 'waitForFunction';
if (name === 'eventInit' || name === 'expectedValue' || (name === 'arg' && isEval))
value = parseSerializedValue(value.value, new Array(10).fill({ handle: '<handle>' }));
if ((name === 'value' && isEval) || (name === 'received' && metadata.method === 'expect'))
if ((name === 'value' && isEval) || (name === 'received' && event.method === 'expect'))
value = parseSerializedValue(value, new Array(10).fill({ handle: '<handle>' }));
if (name === 'selector')
return { text: asLocator(sdkLanguage || 'javascript', metadata.params.selector), type: 'locator', name: 'locator' };
return { text: asLocator(sdkLanguage || 'javascript', event.params.selector), type: 'locator', name: 'locator' };
const type = typeof value;
if (type !== 'object' || value === null)
return { text: String(value), type, name };

View File

@ -29,14 +29,14 @@ export const ConsoleTab: React.FunctionComponent<{
const entries: { message?: channels.ConsoleMessageInitializer, error?: channels.SerializedError }[] = [];
const context = modelUtil.context(action);
for (const event of modelUtil.eventsForAction(action)) {
if (event.metadata.method !== 'console' && event.metadata.method !== 'pageError')
if (event.method !== 'console' && event.method !== 'pageError')
continue;
if (event.metadata.method === 'console') {
const { guid } = event.metadata.params.message;
entries.push({ message: context.objects[guid] });
if (event.method === 'console') {
const { guid } = event.params.message;
entries.push({ message: context.initializers[guid] });
}
if (event.metadata.method === 'pageError')
entries.push({ error: event.metadata.params.error });
if (event.method === 'pageError')
entries.push({ error: event.params.error });
}
return entries;
}, [action]);

View File

@ -17,7 +17,7 @@
import type { Language } from '@isomorphic/locatorGenerators';
import type { ResourceSnapshot } from '@trace/snapshot';
import type * as trace from '@trace/trace';
import type { ActionTraceEvent } from '@trace/trace';
import type { ActionTraceEvent, EventTraceEvent } from '@trace/trace';
import type { ContextEntry, PageEntry } from '../entries';
const contextSymbol = Symbol('context');
@ -35,7 +35,7 @@ export class MultiTraceModel {
readonly options: trace.BrowserContextEventOptions;
readonly pages: PageEntry[];
readonly actions: trace.ActionTraceEvent[];
readonly events: trace.ActionTraceEvent[];
readonly events: trace.EventTraceEvent[];
readonly hasSource: boolean;
readonly sdkLanguage: Language | undefined;
readonly testIdAttributeName: string | undefined;
@ -54,11 +54,11 @@ export class MultiTraceModel {
this.endTime = contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE);
this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages));
this.actions = ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.actions));
this.events = ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.events));
this.events = ([] as EventTraceEvent[]).concat(...contexts.map(c => c.events));
this.hasSource = contexts.some(c => c.hasSource);
this.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
this.events.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
this.actions.sort((a1, a2) => a1.startTime - a2.startTime);
this.events.sort((a1, a2) => a1.time - a2.time);
}
}
@ -87,28 +87,28 @@ export function stats(action: ActionTraceEvent): { errors: number, warnings: num
let warnings = 0;
const c = context(action);
for (const event of eventsForAction(action)) {
if (event.metadata.method === 'console') {
const { guid } = event.metadata.params.message;
const type = c.objects[guid]?.type;
if (event.method === 'console') {
const { guid } = event.params.message;
const type = c.initializers[guid]?.type;
if (type === 'warning')
++warnings;
else if (type === 'error')
++errors;
}
if (event.metadata.method === 'pageError')
if (event.method === 'pageError')
++errors;
}
return { errors, warnings };
}
export function eventsForAction(action: ActionTraceEvent): ActionTraceEvent[] {
let result: ActionTraceEvent[] = (action as any)[eventsSymbol];
export function eventsForAction(action: ActionTraceEvent): EventTraceEvent[] {
let result: EventTraceEvent[] = (action as any)[eventsSymbol];
if (result)
return result;
const nextAction = next(action);
result = context(action).events.filter(event => {
return event.metadata.startTime >= action.metadata.startTime && (!nextAction || event.metadata.startTime < nextAction.metadata.startTime);
return event.time >= action.startTime && (!nextAction || event.time < nextAction.startTime);
});
(action as any)[eventsSymbol] = result;
return result;
@ -121,7 +121,7 @@ export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[]
const nextAction = next(action);
result = context(action).resources.filter(resource => {
return typeof resource._monotonicTime === 'number' && resource._monotonicTime > action.metadata.startTime && (!nextAction || resource._monotonicTime < nextAction.metadata.startTime);
return typeof resource._monotonicTime === 'number' && resource._monotonicTime > action.startTime && (!nextAction || resource._monotonicTime < nextAction.startTime);
});
(action as any)[resourcesSymbol] = result;
return result;

View File

@ -42,7 +42,7 @@ export const SnapshotTab: React.FunctionComponent<{
const [pickerVisible, setPickerVisible] = React.useState(false);
const snapshotMap = new Map<string, { title: string, snapshotName: string }>();
for (const snapshot of action?.metadata.snapshots || [])
for (const snapshot of action?.snapshots || [])
snapshotMap.set(snapshot.title, snapshot);
const actionSnapshot = snapshotMap.get('action') || snapshotMap.get('after');
const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[];
@ -58,11 +58,11 @@ export const SnapshotTab: React.FunctionComponent<{
const params = new URLSearchParams();
params.set('trace', context(action).traceUrl);
params.set('name', snapshot.snapshotName);
snapshotUrl = new URL(`snapshot/${action.metadata.pageId}?${params.toString()}`, window.location.href).toString();
snapshotInfoUrl = new URL(`snapshotInfo/${action.metadata.pageId}?${params.toString()}`, window.location.href).toString();
snapshotUrl = new URL(`snapshot/${action.pageId}?${params.toString()}`, window.location.href).toString();
snapshotInfoUrl = new URL(`snapshotInfo/${action.pageId}?${params.toString()}`, window.location.href).toString();
if (snapshot.snapshotName.includes('action')) {
pointX = action.metadata.point?.x;
pointY = action.metadata.point?.y;
pointX = action.point?.x;
pointY = action.point?.y;
}
const popoutParams = new URLSearchParams();
popoutParams.set('r', snapshotUrl);

View File

@ -44,10 +44,7 @@ export const SourceTab: React.FunctionComponent<{
const stackInfo = React.useMemo<StackInfo>(() => {
if (!action)
return '';
const { metadata } = action;
if (!metadata.stack)
return '';
const frames = metadata.stack;
const frames = action.stack || [];
return {
frames,
fileContent: new Map(),

View File

@ -24,7 +24,7 @@ export const StackTraceView: React.FunctionComponent<{
selectedFrame: number,
setSelectedFrame: (index: number) => void
}> = ({ action, setSelectedFrame, selectedFrame }) => {
const frames = action?.metadata.stack || [];
const frames = action?.stack || [];
return <ListView
dataTestId='stack-trace'
items={frames}

View File

@ -15,7 +15,7 @@
limitations under the License.
*/
import type { ActionTraceEvent } from '@trace/trace';
import type { ActionTraceEvent, EventTraceEvent } from '@trace/trace';
import { msToString } from '@web/uiUtils';
import * as React from 'react';
import type { Boundaries } from '../geometry';
@ -26,7 +26,7 @@ import './timeline.css';
type TimelineBar = {
action?: ActionTraceEvent;
event?: ActionTraceEvent;
event?: EventTraceEvent;
leftPosition: number;
rightPosition: number;
leftTime: number;
@ -56,34 +56,34 @@ export const Timeline: React.FunctionComponent<{
const bars = React.useMemo(() => {
const bars: TimelineBar[] = [];
for (const entry of context.actions) {
let detail = trimRight(entry.metadata.params.selector || '', 50);
if (entry.metadata.method === 'goto')
detail = trimRight(entry.metadata.params.url || '', 50);
let detail = trimRight(entry.params.selector || '', 50);
if (entry.method === 'goto')
detail = trimRight(entry.params.url || '', 50);
bars.push({
action: entry,
leftTime: entry.metadata.startTime,
rightTime: entry.metadata.endTime,
leftPosition: timeToPosition(measure.width, boundaries, entry.metadata.startTime),
rightPosition: timeToPosition(measure.width, boundaries, entry.metadata.endTime),
label: entry.metadata.apiName + ' ' + detail,
title: entry.metadata.endTime ? msToString(entry.metadata.endTime - entry.metadata.startTime) : 'Timed Out',
type: entry.metadata.type + '.' + entry.metadata.method,
className: `${entry.metadata.type}_${entry.metadata.method}`.toLowerCase()
leftTime: entry.startTime,
rightTime: entry.endTime,
leftPosition: timeToPosition(measure.width, boundaries, entry.startTime),
rightPosition: timeToPosition(measure.width, boundaries, entry.endTime),
label: entry.apiName + ' ' + detail,
title: entry.endTime ? msToString(entry.endTime - entry.startTime) : 'Timed Out',
type: entry.type + '.' + entry.method,
className: `${entry.type}_${entry.method}`.toLowerCase()
});
}
for (const event of context.events) {
const startTime = event.metadata.startTime;
const startTime = event.time;
bars.push({
event,
leftTime: startTime,
rightTime: startTime,
leftPosition: timeToPosition(measure.width, boundaries, startTime),
rightPosition: timeToPosition(measure.width, boundaries, startTime),
label: event.metadata.method,
title: event.metadata.endTime ? msToString(event.metadata.endTime - event.metadata.startTime) : 'Timed Out',
type: event.metadata.type + '.' + event.metadata.method,
className: `${event.metadata.type}_${event.metadata.method}`.toLowerCase()
label: event.method,
title: '0',
type: event.class + '.' + event.method,
className: `${event.class}_${event.method}`.toLowerCase()
});
}
return bars;
@ -237,5 +237,5 @@ function trimRight(s: string, maxLength: number): string {
const kBarHeight = 11;
function barTop(bar: TimelineBar): number {
return bar.event ? 22 : (bar.action?.metadata.method === 'waitForEventInfo' ? 0 : 11);
return bar.event ? 22 : (bar.action?.method === 'waitForEventInfo' ? 0 : 11);
}

View File

@ -0,0 +1,162 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Entry as ResourceSnapshot } from '../../../trace/src/har';
type SerializedValue = {
n?: number,
b?: boolean,
s?: string,
v?: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0',
d?: string,
u?: string,
r?: {
p: string,
f: string,
},
a?: SerializedValue[],
o?: {
k: string,
v: SerializedValue,
}[],
h?: number,
id?: number,
ref?: number,
};
type Point = {
x: number,
y: number,
};
type StackFrame = {
file: string,
line: number,
column: number,
function?: string,
};
type SerializedError = {
error?: {
message: string,
name: string,
stack?: string,
},
value?: SerializedValue,
};
type CallMetadata = {
id: string;
startTime: number;
endTime: number;
pauseStartTime?: number;
pauseEndTime?: number;
type: string;
method: string;
params: any;
apiName?: string;
internal?: boolean;
isServerSide?: boolean;
wallTime?: number;
location?: { file: string, line?: number, column?: number };
log: string[];
afterSnapshot?: string;
snapshots: { title: string, snapshotName: string }[];
error?: SerializedError;
result?: any;
point?: Point;
objectId?: string;
pageId?: string;
frameId?: string;
};
export type NodeSnapshot =
string |
[ [number, number] ] |
[ string ] |
[ string, { [attr: string]: string }, ...any ];
export type ResourceOverride = {
url: string,
sha1?: string,
ref?: number
};
export type FrameSnapshot = {
snapshotName?: string,
pageId: string,
frameId: string,
frameUrl: string,
timestamp: number,
collectionTime: number,
doctype?: string,
html: NodeSnapshot,
resourceOverrides: ResourceOverride[],
viewport: { width: number, height: number },
isMainFrame: boolean,
};
export type BrowserContextEventOptions = {
viewport?: { width: number, height: number },
deviceScaleFactor?: number,
isMobile?: boolean,
userAgent?: string,
};
export type ContextCreatedTraceEvent = {
version: number,
type: 'context-options',
browserName: string,
platform: string,
wallTime: number,
title?: string,
options: BrowserContextEventOptions,
sdkLanguage?: 'javascript' | 'python' | 'java' | 'csharp',
testIdAttributeName?: string,
};
export type ScreencastFrameTraceEvent = {
type: 'screencast-frame',
pageId: string,
sha1: string,
width: number,
height: number,
timestamp: number,
};
export type ActionTraceEvent = {
type: 'action' | 'event',
metadata: CallMetadata & { stack?: StackFrame[] },
};
export type ResourceSnapshotTraceEvent = {
type: 'resource-snapshot',
snapshot: ResourceSnapshot,
};
export type FrameSnapshotTraceEvent = {
type: 'frame-snapshot',
snapshot: FrameSnapshot,
};
export type TraceEvent =
ContextCreatedTraceEvent |
ScreencastFrameTraceEvent |
ActionTraceEvent |
ResourceSnapshotTraceEvent |
FrameSnapshotTraceEvent;

View File

@ -14,15 +14,14 @@
* limitations under the License.
*/
import type { CallMetadata } from '@protocol/callMetadata';
import type { StackFrame } from '@protocol/channels';
import type { Point, SerializedError, StackFrame } from '@protocol/channels';
import type { Language } from '../../playwright-core/src/server/isomorphic/locatorGenerators';
import type { FrameSnapshot, ResourceSnapshot } from './snapshot';
export type Size = { width: number, height: number };
// Make sure you add _modernize_N_to_N1(event: any) to traceModel.ts.
export type VERSION = 3;
export type VERSION = 4;
export type BrowserContextEventOptions = {
viewport?: Size,
@ -53,8 +52,38 @@ export type ScreencastFrameTraceEvent = {
};
export type ActionTraceEvent = {
type: 'action' | 'event',
metadata: CallMetadata & { stack?: StackFrame[] },
type: 'action',
callId: string;
startTime: number;
endTime: number;
apiName: string;
class: string;
method: string;
params: any;
wallTime: number;
log: string[];
snapshots: { title: string, snapshotName: string }[];
stack?: StackFrame[];
error?: SerializedError['error'];
result?: any;
point?: Point;
pageId?: string;
};
export type EventTraceEvent = {
type: 'event',
time: number;
class: string;
method: string;
params: any;
pageId?: string;
};
export type ObjectTraceEvent = {
type: 'object';
class: string;
initializer: any;
guid: string;
};
export type ResourceSnapshotTraceEvent = {
@ -71,5 +100,7 @@ export type TraceEvent =
ContextCreatedTraceEvent |
ScreencastFrameTraceEvent |
ActionTraceEvent |
EventTraceEvent |
ObjectTraceEvent |
ResourceSnapshotTraceEvent |
FrameSnapshotTraceEvent;

View File

@ -18,6 +18,7 @@ import type { Frame, Page } from 'playwright-core';
import { ZipFile } from '../../packages/playwright-core/lib/utils/zipFile';
import type { StackFrame } from '@protocol/channels';
import { parseClientSideCallMetadata } from '../../packages/trace/src/traceUtils';
import type { ActionTraceEvent } from '../../packages/trace/src/trace';
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
const handle = await page.evaluateHandle(async ({ frameId, url }) => {
@ -28,20 +29,20 @@ export async function attachFrame(page: Page, frameId: string, url: string): Pro
await new Promise(x => frame.onload = x);
return frame;
}, { frameId, url });
return handle.asElement().contentFrame();
return handle.asElement().contentFrame() as Promise<Frame>;
}
export async function detachFrame(page: Page, frameId: string) {
await page.evaluate(frameId => {
document.getElementById(frameId).remove();
document.getElementById(frameId)!.remove();
}, frameId);
}
export async function verifyViewport(page: Page, width: number, height: number) {
// `expect` may clash in test runner tests if imported eagerly.
const { expect } = require('@playwright/test');
expect(page.viewportSize().width).toBe(width);
expect(page.viewportSize().height).toBe(height);
expect(page.viewportSize()!.width).toBe(width);
expect(page.viewportSize()!.height).toBe(height);
expect(await page.evaluate('window.innerWidth')).toBe(width);
expect(await page.evaluate('window.innerHeight')).toBe(height);
}
@ -100,18 +101,18 @@ export async function parseTrace(file: string): Promise<{ events: any[], resourc
resources.set(entry, await zipFS.read(entry));
zipFS.close();
const events = [];
for (const line of resources.get('trace.trace').toString().split('\n')) {
const events: any[] = [];
for (const line of resources.get('trace.trace')!.toString().split('\n')) {
if (line)
events.push(JSON.parse(line));
}
for (const line of resources.get('trace.network').toString().split('\n')) {
for (const line of resources.get('trace.network')!.toString().split('\n')) {
if (line)
events.push(JSON.parse(line));
}
const stacks = parseClientSideCallMetadata(JSON.parse(resources.get('trace.stacks').toString()));
const stacks = parseClientSideCallMetadata(JSON.parse(resources.get('trace.stacks')!.toString()));
return {
events,
resources,
@ -120,11 +121,11 @@ export async function parseTrace(file: string): Promise<{ events: any[], resourc
};
}
function eventsToActions(events: any[]): string[] {
function eventsToActions(events: ActionTraceEvent[]): string[] {
// Trace viewer only shows non-internal non-tracing actions.
return events.filter(e => e.type === 'action' && !e.metadata.internal && !e.metadata.method.startsWith('tracing'))
.sort((a, b) => a.metadata.startTime - b.metadata.startTime)
.map(e => e.metadata.apiName);
return events.filter(e => e.type === 'action')
.sort((a, b) => a.startTime - b.startTime)
.map(e => e.apiName);
}
export async function parseHar(file: string): Promise<Map<string, Buffer>> {

View File

@ -20,6 +20,7 @@ import path from 'path';
import { browserTest, contextTest as test, expect } from '../config/browserTest';
import { parseTrace } from '../config/utils';
import type { StackFrame } from '@protocol/channels';
import type { ActionTraceEvent } from '../../packages/trace/src/trace';
test.skip(({ trace }) => trace === 'on');
@ -111,9 +112,9 @@ test('should not include buffers in the trace', async ({ context, page, server,
await page.screenshot();
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
const screenshotEvent = events.find(e => e.type === 'action' && e.metadata.apiName === 'page.screenshot');
expect(screenshotEvent.metadata.snapshots.length).toBe(2);
expect(screenshotEvent.metadata.result).toEqual({});
const screenshotEvent = events.find(e => e.type === 'action' && e.apiName === 'page.screenshot');
expect(screenshotEvent.snapshots.length).toBe(2);
expect(screenshotEvent.result).toEqual({});
});
test('should exclude internal pages', async ({ browserName, context, page, server }, testInfo) => {
@ -127,7 +128,7 @@ test('should exclude internal pages', async ({ browserName, context, page, serve
const trace = await parseTrace(testInfo.outputPath('trace.zip'));
const pageIds = new Set();
trace.events.forEach(e => {
const pageId = e.metadata?.pageId;
const pageId = e.pageId;
if (pageId)
pageIds.add(pageId);
});
@ -139,7 +140,7 @@ test('should include context API requests', async ({ browserName, context, page,
await page.request.post(server.PREFIX + '/simple.json', { data: { foo: 'bar' } });
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
const postEvent = events.find(e => e.metadata?.apiName === 'apiRequestContext.post');
const postEvent = events.find(e => e.apiName === 'apiRequestContext.post');
expect(postEvent).toBeTruthy();
const harEntry = events.find(e => e.type === 'resource-snapshot');
expect(harEntry).toBeTruthy();
@ -350,9 +351,9 @@ test('should include interrupted actions', async ({ context, page, server }, tes
await context.close();
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
const clickEvent = events.find(e => e.metadata?.apiName === 'page.click');
const clickEvent = events.find(e => e.apiName === 'page.click');
expect(clickEvent).toBeTruthy();
expect(clickEvent.metadata.error.error.message).toBe('Action was interrupted');
expect(clickEvent.error.message).toBe('Action was interrupted');
});
test('should throw when starting with different options', async ({ context }) => {
@ -395,8 +396,8 @@ test('should work with multiple chunks', async ({ context, page, server }, testI
'page.click',
'page.click',
]);
expect(trace1.events.find(e => e.metadata?.apiName === 'page.click' && !!e.metadata.error)).toBeTruthy();
expect(trace1.events.find(e => e.metadata?.apiName === 'page.click' && e.metadata?.error?.error?.message === 'Action was interrupted')).toBeTruthy();
expect(trace1.events.find(e => e.apiName === 'page.click' && !!e.error)).toBeTruthy();
expect(trace1.events.find(e => e.apiName === 'page.click' && e.error?.message === 'Action was interrupted')).toBeTruthy();
expect(trace1.events.some(e => e.type === 'frame-snapshot')).toBeTruthy();
expect(trace1.events.some(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('style.css'))).toBeTruthy();
@ -472,7 +473,7 @@ test('should hide internal stack frames', async ({ context, page }, testInfo) =>
await context.tracing.stop({ path: tracePath });
const trace = await parseTrace(tracePath);
const actions = trace.events.filter(e => e.type === 'action' && !e.metadata.apiName.startsWith('tracing.'));
const actions = trace.events.filter(e => e.type === 'action' && !e.apiName.startsWith('tracing.'));
expect(actions).toHaveLength(4);
for (const action of actions)
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
@ -493,7 +494,7 @@ test('should hide internal stack frames in expect', async ({ context, page }, te
await context.tracing.stop({ path: tracePath });
const trace = await parseTrace(tracePath);
const actions = trace.events.filter(e => e.type === 'action' && !e.metadata.apiName.startsWith('tracing.'));
const actions = trace.events.filter(e => e.type === 'action' && !e.apiName.startsWith('tracing.'));
expect(actions).toHaveLength(5);
for (const action of actions)
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
@ -606,7 +607,7 @@ function expectBlue(pixels: Buffer, offset: number) {
expect(a).toBe(255);
}
function relativeStack(action: any, stacks: Map<string, StackFrame[]>): string[] {
const stack = stacks.get(action.metadata.id) || [];
function relativeStack(action: ActionTraceEvent, stacks: Map<string, StackFrame[]>): string[] {
const stack = stacks.get(action.callId) || [];
return stack.map(f => f.file.replace(__dirname + path.sep, ''));
}