From 2af7d672efeef92ca45577b2c3eeef3de6edb40f Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 19 Sep 2023 16:21:09 -0700 Subject: [PATCH] fix(tracing): bump trace version to V5, migrate V4 traces to consoleMessage.args (#27162) This moves the fix in #27095 from `modernize` to `appendEvent`. The reason is that `trace V4` is used both for older traces that do not have `consoleMessage.args` and the new ones with `args`. Since we do not call `modernize` for traces of the same version, the original fix does not help in this case. Fixes #27144. --- .../src/server/trace/recorder/tracing.ts | 28 +-- packages/trace-viewer/src/entries.ts | 4 +- packages/trace-viewer/src/traceModel.ts | 72 ++++-- packages/trace-viewer/src/ui/consoleTab.tsx | 38 ++- packages/trace-viewer/src/ui/modelUtil.ts | 20 +- packages/trace-viewer/src/versions/traceV4.ts | 225 ++++++++++++++++++ packages/trace/src/trace.ts | 26 +- tests/assets/trace-1.37.zip | Bin 0 -> 8630 bytes tests/config/utils.ts | 4 +- tests/library/trace-viewer.spec.ts | 12 + tests/library/tracing.spec.ts | 2 +- 11 files changed, 340 insertions(+), 91 deletions(-) create mode 100644 packages/trace-viewer/src/versions/traceV4.ts create mode 100644 tests/assets/trace-1.37.zip diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 4b791b3f84..933f9d9ba9 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -43,7 +43,7 @@ import { Snapshotter } from './snapshotter'; import { yazl } from '../../../zipBundle'; import type { ConsoleMessage } from '../../console'; -const version: trace.VERSION = 4; +const version: trace.VERSION = 5; export type TracerOptions = { name?: string; @@ -429,24 +429,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps } private _onConsoleMessage(message: ConsoleMessage) { - const object: trace.ConsoleMessageTraceEvent = { - type: 'object', - class: 'ConsoleMessage', - guid: message.guid, - initializer: { - type: message.type(), - text: message.text(), - args: message.args().map(a => ({ preview: a.toString(), value: a.rawValue() })), - location: message.location(), - }, - }; - this._appendTraceEvent(object); - - const event: trace.EventTraceEvent = { - type: 'event', - class: 'BrowserContext', - method: 'console', - params: { message: { guid: message.guid } }, + const event: trace.ConsoleMessageTraceEvent = { + type: 'console', + messageType: message.type(), + text: message.text(), + args: message.args().map(a => ({ preview: a.toString(), value: a.rawValue() })), + location: message.location(), time: monotonicTime(), pageId: message.page().guid, }; @@ -478,7 +466,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps private _appendTraceEvent(event: trace.TraceEvent) { const visited = visitTraceEvent(event, this._state!.traceSha1s); // Do not flush (console) events, they are too noisy, unless we are in ui mode (live). - const flush = this._state!.options.live || (event.type !== 'event' && event.type !== 'object'); + const flush = this._state!.options.live || (event.type !== 'event' && event.type !== 'console'); this._fs.appendFile(this._state!.traceFile, JSON.stringify(visited) + '\n', flush); } diff --git a/packages/trace-viewer/src/entries.ts b/packages/trace-viewer/src/entries.ts index cc7abc06e4..af44989733 100644 --- a/packages/trace-viewer/src/entries.ts +++ b/packages/trace-viewer/src/entries.ts @@ -34,9 +34,8 @@ export type ContextEntry = { pages: PageEntry[]; resources: ResourceSnapshot[]; actions: trace.ActionTraceEvent[]; - events: trace.EventTraceEvent[]; + events: (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[]; stdio: trace.StdioTraceEvent[]; - initializers: { [key: string]: trace.ConsoleMessageTraceEvent['initializer'] }; hasSource: boolean; }; @@ -65,7 +64,6 @@ export function createEmptyContext(): ContextEntry { actions: [], events: [], stdio: [], - initializers: {}, hasSource: false }; } diff --git a/packages/trace-viewer/src/traceModel.ts b/packages/trace-viewer/src/traceModel.ts index 99e783c915..042648d654 100644 --- a/packages/trace-viewer/src/traceModel.ts +++ b/packages/trace-viewer/src/traceModel.ts @@ -16,6 +16,7 @@ import type * as trace from '@trace/trace'; import type * as traceV3 from './versions/traceV3'; +import type * as traceV4 from './versions/traceV4'; import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils'; import type { ContextEntry, PageEntry } from './entries'; import { createEmptyContext } from './entries'; @@ -39,6 +40,7 @@ export class TraceModel { private _attachments = new Map(); private _resourceToContentType = new Map(); private _jsHandles = new Map(); + private _consoleObjects = new Map(); constructor() { } @@ -114,6 +116,7 @@ export class TraceModel { this._snapshotStorage!.finalize(); this._jsHandles.clear(); + this._consoleObjects.clear(); } async hasEntry(filename: string): Promise { @@ -209,8 +212,8 @@ export class TraceModel { contextEntry!.stdio.push(event); break; } - case 'object': { - contextEntry!.initializers[event.guid] = event.initializer; + case 'console': { + contextEntry!.events.push(event); break; } case 'resource-snapshot': @@ -235,12 +238,15 @@ export class TraceModel { } } - private _modernize(event: any): trace.TraceEvent { + private _modernize(event: any): trace.TraceEvent | null { if (this._version === undefined) return event; - const lastVersion: trace.VERSION = 4; - for (let version = this._version; version < lastVersion; ++version) + const lastVersion: trace.VERSION = 5; + for (let version = this._version; version < lastVersion; ++version) { event = (this as any)[`_modernize_${version}_to_${version + 1}`].call(this, event); + if (!event) + return null; + } return event; } @@ -286,7 +292,7 @@ export class TraceModel { return event; } - _modernize_3_to_4(event: traceV3.TraceEvent): trace.TraceEvent | null { + _modernize_3_to_4(event: traceV3.TraceEvent): traceV4.TraceEvent | null { if (event.type !== 'action' && event.type !== 'event') { return event as traceV3.ContextCreatedTraceEvent | traceV3.ScreencastFrameTraceEvent | @@ -299,23 +305,12 @@ export class TraceModel { return null; if (event.type === 'event') { - if (metadata.method === '__create__' && metadata.type === 'JSHandle') - this._jsHandles.set(metadata.params.guid, metadata.params.initializer); if (metadata.method === '__create__' && metadata.type === 'ConsoleMessage') { return { type: 'object', class: metadata.type, guid: metadata.params.guid, - initializer: { - ...metadata.params.initializer, - args: metadata.params.initializer.args?.map((arg: any) => { - if (arg.guid) { - const handle = this._jsHandles.get(arg.guid); - return { preview: handle?.preview || '', value: '' }; - } - return { preview: '', value: '' }; - }) - }, + initializer: metadata.params.initializer, }; } return { @@ -348,6 +343,47 @@ export class TraceModel { pageId: metadata.pageId, }; } + + _modernize_4_to_5(event: traceV4.TraceEvent): trace.TraceEvent | null { + if (event.type === 'event' && event.method === '__create__' && event.class === 'JSHandle') + this._jsHandles.set(event.params.guid, event.params.initializer); + if (event.type === 'object') { + // We do not expect any other 'object' events. + if (event.class !== 'ConsoleMessage') + return null; + // Older traces might have `args` inherited from the protocol initializer - guid of JSHandle, + // but might also have modern `args` with preview and value. + const args: { preview: string, value: string }[] = (event.initializer as any).args?.map((arg: any) => { + if (arg.guid) { + const handle = this._jsHandles.get(arg.guid); + return { preview: handle?.preview || '', value: '' }; + } + return { preview: arg.preview || '', value: arg.value || '' }; + }); + this._consoleObjects.set(event.guid, { + type: event.initializer.type, + text: event.initializer.text, + location: event.initializer.location, + args, + }); + return null; + } + if (event.type === 'event' && event.method === 'console') { + const consoleMessage = this._consoleObjects.get(event.params.message?.guid || ''); + if (!consoleMessage) + return null; + return { + type: 'console', + time: event.time, + pageId: event.pageId, + messageType: consoleMessage.type, + text: consoleMessage.text, + args: consoleMessage.args, + location: consoleMessage.location, + }; + } + return event; + } } function stripEncodingFromContentType(contentType: string) { diff --git a/packages/trace-viewer/src/ui/consoleTab.tsx b/packages/trace-viewer/src/ui/consoleTab.tsx index fcb1cd79cc..5c95e154d0 100644 --- a/packages/trace-viewer/src/ui/consoleTab.tsx +++ b/packages/trace-viewer/src/ui/consoleTab.tsx @@ -17,7 +17,7 @@ import type * as channels from '@protocol/channels'; import * as React from 'react'; import './consoleTab.css'; -import * as modelUtil from './modelUtil'; +import type * as modelUtil from './modelUtil'; import { ListView } from '@web/components/listView'; import type { Boundaries } from '../geometry'; import { msToString } from '@web/uiUtils'; @@ -51,29 +51,23 @@ export function useConsoleTabModel(model: modelUtil.MultiTraceModel | undefined, return { entries: [] }; const entries: ConsoleEntry[] = []; for (const event of model.events) { - if (event.method !== 'console' && event.method !== 'pageError') - continue; - if (event.method === 'console') { - const { guid } = event.params.message; - const browserMessage = modelUtil.context(event).initializers[guid]; - if (browserMessage) { - const body = browserMessage.args && browserMessage.args.length ? format(browserMessage.args) : formatAnsi(browserMessage.text); - const url = browserMessage.location.url; - const filename = url ? url.substring(url.lastIndexOf('/') + 1) : ''; - const location = `${filename}:${browserMessage.location.lineNumber}`; + if (event.type === 'console') { + const body = event.args && event.args.length ? format(event.args) : formatAnsi(event.text); + const url = event.location.url; + const filename = url ? url.substring(url.lastIndexOf('/') + 1) : ''; + const location = `${filename}:${event.location.lineNumber}`; - entries.push({ - browserMessage: { - body, - location, - }, - isError: modelUtil.context(event).initializers[guid]?.type === 'error', - isWarning: modelUtil.context(event).initializers[guid]?.type === 'warning', - timestamp: event.time, - }); - } + entries.push({ + browserMessage: { + body, + location, + }, + isError: event.messageType === 'error', + isWarning: event.messageType === 'warning', + timestamp: event.time, + }); } - if (event.method === 'pageError') { + if (event.type === 'event' && event.method === 'pageError') { entries.push({ browserError: event.params.error, isError: true, diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index dd42c8ac56..9f1c466404 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -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, EventTraceEvent } from '@trace/trace'; +import type { ActionTraceEvent } from '@trace/trace'; import type { ContextEntry, PageEntry } from '../entries'; const contextSymbol = Symbol('context'); @@ -58,7 +58,7 @@ export class MultiTraceModel { readonly options: trace.BrowserContextEventOptions; readonly pages: PageEntry[]; readonly actions: ActionTraceEventInContext[]; - readonly events: trace.EventTraceEvent[]; + readonly events: (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[]; readonly stdio: trace.StdioTraceEvent[]; readonly hasSource: boolean; readonly sdkLanguage: Language | undefined; @@ -83,7 +83,7 @@ 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 = mergeActions(contexts); - this.events = ([] as EventTraceEvent[]).concat(...contexts.map(c => c.events)); + this.events = ([] as (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[]).concat(...contexts.map(c => c.events)); this.stdio = ([] as trace.StdioTraceEvent[]).concat(...contexts.map(c => c.stdio)); this.hasSource = contexts.some(c => c.hasSource); this.resources = [...contexts.map(c => c.resources)].flat(); @@ -203,7 +203,7 @@ export function idForAction(action: ActionTraceEvent) { return `${action.pageId || 'none'}:${action.callId}`; } -export function context(action: ActionTraceEvent | EventTraceEvent): ContextEntry { +export function context(action: ActionTraceEvent | trace.EventTraceEvent): ContextEntry { return (action as any)[contextSymbol]; } @@ -218,24 +218,22 @@ export function prevInList(action: ActionTraceEvent): ActionTraceEvent { export function stats(action: ActionTraceEvent): { errors: number, warnings: number } { let errors = 0; let warnings = 0; - const c = context(action); for (const event of eventsForAction(action)) { - if (event.method === 'console') { - const { guid } = event.params.message; - const type = c.initializers[guid]?.type; + if (event.type === 'console') { + const type = event.messageType; if (type === 'warning') ++warnings; else if (type === 'error') ++errors; } - if (event.method === 'pageError') + if (event.type === 'event' && event.method === 'pageError') ++errors; } return { errors, warnings }; } -export function eventsForAction(action: ActionTraceEvent): EventTraceEvent[] { - let result: EventTraceEvent[] = (action as any)[eventsSymbol]; +export function eventsForAction(action: ActionTraceEvent): (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[] { + let result: (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[] = (action as any)[eventsSymbol]; if (result) return result; diff --git a/packages/trace-viewer/src/versions/traceV4.ts b/packages/trace-viewer/src/versions/traceV4.ts new file mode 100644 index 0000000000..982bea1492 --- /dev/null +++ b/packages/trace-viewer/src/versions/traceV4.ts @@ -0,0 +1,225 @@ +/** + * 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 Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl'; +type Point = { x: number, y: number }; +type Size = { width: number, height: number }; + +type StackFrame = { + file: string, + line: number, + column: number, + function?: string, +}; + +type SerializedValue = { + n?: number, + b?: boolean, + s?: string, + v?: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0', + d?: string, + u?: string, + bi?: string, + m?: SerializedValue, + se?: SerializedValue, + r?: { + p: string, + f: string, + }, + a?: SerializedValue[], + o?: { + k: string, + v: SerializedValue, + }[], + h?: number, + id?: number, + ref?: number, +}; + +type SerializedError = { + error?: { + message: string, + name: string, + stack?: string, + }, + value?: SerializedValue, +}; + +type NodeSnapshot = + // Text node. + string | + // Subtree reference, "x snapshots ago, node #y". Could point to a text node. + // Only nodes that are not references are counted, starting from zero, using post-order traversal. + [ [number, number] ] | + // Just node name. + [ string ] | + // Node name, attributes, child nodes. + // Unfortunately, we cannot make this type definition recursive, therefore "any". + [ string, { [attr: string]: string }, ...any ]; + + +type ResourceOverride = { + url: string, + sha1?: string, + ref?: number +}; + +type FrameSnapshot = { + snapshotName?: string, + callId: string, + pageId: string, + frameId: string, + frameUrl: string, + timestamp: number, + collectionTime: number, + doctype?: string, + html: NodeSnapshot, + resourceOverrides: ResourceOverride[], + viewport: { width: number, height: number }, + isMainFrame: boolean, +}; + +type BrowserContextEventOptions = { + viewport?: Size, + deviceScaleFactor?: number, + isMobile?: boolean, + userAgent?: string, +}; + +type ContextCreatedTraceEvent = { + version: number, + type: 'context-options', + browserName: string, + channel?: string, + platform: string, + wallTime: number, + title?: string, + options: BrowserContextEventOptions, + sdkLanguage?: Language, + testIdAttributeName?: string, +}; + +type ScreencastFrameTraceEvent = { + type: 'screencast-frame', + pageId: string, + sha1: string, + width: number, + height: number, + timestamp: number, +}; + +type BeforeActionTraceEvent = { + type: 'before', + callId: string; + startTime: number; + apiName: string; + class: string; + method: string; + params: Record; + wallTime: number; + beforeSnapshot?: string; + stack?: StackFrame[]; + pageId?: string; + parentId?: string; +}; + +type InputActionTraceEvent = { + type: 'input', + callId: string; + inputSnapshot?: string; + point?: Point; +}; + +type AfterActionTraceEventAttachment = { + name: string; + contentType: string; + path?: string; + sha1?: string; + base64?: string; +}; + +type AfterActionTraceEvent = { + type: 'after', + callId: string; + endTime: number; + afterSnapshot?: string; + log: string[]; + error?: SerializedError['error']; + attachments?: AfterActionTraceEventAttachment[]; + result?: any; +}; + +type EventTraceEvent = { + type: 'event', + time: number; + class: string; + method: string; + params: any; + pageId?: string; +}; + +type ConsoleMessageTraceEvent = { + type: 'object'; + class: string; + initializer: { + type: string, + text: string, + location: { + url: string, + lineNumber: number, + columnNumber: number, + }, + }; + guid: string; +}; + +type ResourceSnapshotTraceEvent = { + type: 'resource-snapshot', + snapshot: ResourceSnapshot, +}; + +type FrameSnapshotTraceEvent = { + type: 'frame-snapshot', + snapshot: FrameSnapshot, +}; + +type ActionTraceEvent = { + type: 'action', +} & Omit + & Omit + & Omit; + +type StdioTraceEvent = { + type: 'stdout' | 'stderr'; + timestamp: number; + text?: string; + base64?: string; +}; + +export type TraceEvent = + ContextCreatedTraceEvent | + ScreencastFrameTraceEvent | + ActionTraceEvent | + BeforeActionTraceEvent | + InputActionTraceEvent | + AfterActionTraceEvent | + EventTraceEvent | + ConsoleMessageTraceEvent | + ResourceSnapshotTraceEvent | + FrameSnapshotTraceEvent | + StdioTraceEvent; diff --git a/packages/trace/src/trace.ts b/packages/trace/src/trace.ts index 1c9c65457d..3f831907e4 100644 --- a/packages/trace/src/trace.ts +++ b/packages/trace/src/trace.ts @@ -21,7 +21,7 @@ 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 = 4; +export type VERSION = 5; export type BrowserContextEventOptions = { viewport?: Size, @@ -103,19 +103,17 @@ export type EventTraceEvent = { }; export type ConsoleMessageTraceEvent = { - type: 'object'; - class: string; - initializer: { - type: string, - text: string, - args?: { preview: string, value: any }[], - location: { - url: string, - lineNumber: number, - columnNumber: number, - }, - }; - guid: string; + type: 'console'; + time: number; + pageId?: string; + messageType: string, + text: string, + args?: { preview: string, value: any }[], + location: { + url: string, + lineNumber: number, + columnNumber: number, + }, }; export type ResourceSnapshotTraceEvent = { diff --git a/tests/assets/trace-1.37.zip b/tests/assets/trace-1.37.zip new file mode 100644 index 0000000000000000000000000000000000000000..aad71ad9a82d68687c58abb724b8609c0ad9bd56 GIT binary patch literal 8630 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 YjVn|Ykx_oRf%bR+9&2Q2)W5F&AA!H=DgXcg literal 0 HcmV?d00001 diff --git a/tests/config/utils.ts b/tests/config/utils.ts index 26c1a8e484..db59648457 100644 --- a/tests/config/utils.ts +++ b/tests/config/utils.ts @@ -22,7 +22,7 @@ import { parseClientSideCallMetadata } from '../../packages/playwright-core/lib/ import { TraceModel } from '../../packages/trace-viewer/src/traceModel'; import type { ActionTreeItem } from '../../packages/trace-viewer/src/ui/modelUtil'; import { buildActionTree, MultiTraceModel } from '../../packages/trace-viewer/src/ui/modelUtil'; -import type { ActionTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace'; +import type { ActionTraceEvent, ConsoleMessageTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace'; export async function attachFrame(page: Page, frameId: string, url: string): Promise { const handle = await page.evaluateHandle(async ({ frameId, url }) => { @@ -158,7 +158,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso }; } -export async function parseTrace(file: string): Promise<{ resources: Map, events: EventTraceEvent[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[] }> { +export async function parseTrace(file: string): Promise<{ resources: Map, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[] }> { const backend = new TraceBackend(file); const traceModel = new TraceModel(); await traceModel.load(backend, () => {}); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index c706ea6e1a..e6f256519d 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -911,6 +911,18 @@ test('should open trace-1.31', async ({ showTraceViewer }) => { await expect(snapshot.locator('[__playwright_target__]')).toHaveText(['Submit']); }); +test('should open trace-1.37', async ({ showTraceViewer }) => { + const traceViewer = await showTraceViewer([path.join(__dirname, '../assets/trace-1.37.zip')]); + const snapshot = await traceViewer.snapshotFrame('page.goto'); + await expect(snapshot.locator('div')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); + + await traceViewer.showConsoleTab(); + await expect(traceViewer.consoleLineMessages).toHaveText(['hello {foo: bar}']); + + await traceViewer.showNetworkTab(); + await expect(traceViewer.networkRequests).toContainText([/200GET\/index.htmltext\/html/, /200GET\/style.cssx-unknown/]); +}); + test('should prefer later resource request with the same method', async ({ page, server, runAndTrace }) => { const html = ` diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index 8f92a7d596..4596c8cf00 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -739,7 +739,7 @@ test('should flush console events on tracing stop', async ({ context, page }, te const tracePath = testInfo.outputPath('trace.zip'); await context.tracing.stop({ path: tracePath }); const trace = await parseTraceRaw(tracePath); - const events = trace.events.filter(e => e.method === 'console'); + const events = trace.events.filter(e => e.type === 'console'); expect(events).toHaveLength(100); });