From 0b1598b4672951d919ced4fb1eacafe076f65df8 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 21 Aug 2026 14:29:41 +0100 Subject: [PATCH] feat(trace): load trace files independently, produce a single context entry TraceLoader now loads each .trace, .network and .stacks file into its own entry without matching them by the file name prefix, then merges everything into a single ContextEntry: per-file clock alignment, action merging by stepId, and origin-dependent metadata resolution all move from TraceModel to the loader. Network files derive their time origin from the first resource and reuse the format version learned from trace files. Stacks are applied through a global call id map. --- packages/isomorphic/trace/entries.ts | 28 +++ packages/isomorphic/trace/traceLoader.ts | 224 ++++++++++++++---- packages/isomorphic/trace/traceModel.ts | 155 +++--------- packages/isomorphic/trace/traceModernizer.ts | 7 +- .../src/server/trace/viewer/traceViewer.ts | 4 + .../src/tools/trace/traceUtils.ts | 2 +- packages/trace-viewer/src/sw/main.ts | 2 +- .../src/ui/liveWorkbenchLoader.tsx | 7 +- .../trace-viewer/src/ui/uiModeTraceView.tsx | 8 +- .../trace-viewer/src/ui/workbenchLoader.tsx | 7 +- tests/config/utils.ts | 2 +- 11 files changed, 260 insertions(+), 186 deletions(-) diff --git a/packages/isomorphic/trace/entries.ts b/packages/isomorphic/trace/entries.ts index a762d285bdd54..d0d3b4a2a50de 100644 --- a/packages/isomorphic/trace/entries.ts +++ b/packages/isomorphic/trace/entries.ts @@ -44,10 +44,38 @@ export type ContextEntry = { stdio: trace.StdioTraceEvent[]; errors: trace.ErrorTraceEvent[]; hasSource: boolean; + hasStepData: boolean; testTimeout?: number; annotations?: trace.TraceEventAnnotation[]; }; +export function createEmptyContext(): ContextEntry { + return { + origin: 'testRunner', + startTime: Number.MAX_SAFE_INTEGER, + wallTime: Number.MAX_SAFE_INTEGER, + monotonicTime: 0, + endTime: 0, + browserName: '', + options: { + deviceScaleFactor: 1, + isMobile: false, + viewport: { width: 1280, height: 800 }, + }, + pages: [], + resources: [], + actions: [], + screenshots: [], + ariaSnapshots: [], + videos: [], + events: [], + errors: [], + stdio: [], + hasSource: false, + hasStepData: false, + }; +} + export type PageEntry = { pageId: string, screencastFrames: { diff --git a/packages/isomorphic/trace/traceLoader.ts b/packages/isomorphic/trace/traceLoader.ts index 9daa159473037..874774ddf6778 100644 --- a/packages/isomorphic/trace/traceLoader.ts +++ b/packages/isomorphic/trace/traceLoader.ts @@ -16,10 +16,12 @@ import { parseClientSideCallMetadata } from './traceUtils'; +import { createEmptyContext } from './entries'; import { SnapshotStorage } from './snapshotStorage'; import { TraceModernizer } from './traceModernizer'; -import type { ContextEntry } from './entries'; +import type { ActionEntry, ContextEntry } from './entries'; +import type { StackFrame } from '@trace/trace'; export interface TraceLoaderBackend { entryNames(): Promise; @@ -30,7 +32,7 @@ export interface TraceLoaderBackend { } export class TraceLoader { - contextEntries: ContextEntry[] = []; + contextEntry: ContextEntry = createEmptyContext(); private _snapshotStorage: SnapshotStorage | undefined; private _backend!: TraceLoaderBackend; private _resourceToContentType = new Map(); @@ -42,34 +44,37 @@ export class TraceLoader { this._backend = backend; const prefix = traceFile?.match(/(.+)\.trace$/)?.[1]; - const prefixes: string[] = []; + const traceNames: string[] = []; + const networkNames: string[] = []; + const stacksNames: string[] = []; let hasSource = false; for (const entryName of await this._backend.entryNames()) { - const match = entryName.match(/(.+)\.trace$/); - if (match && (!prefix || prefix === match[1])) - prefixes.push(match[1] || ''); + if (entryName.endsWith('.trace') && (!prefix || entryName === prefix + '.trace')) + traceNames.push(entryName); + if (entryName.endsWith('.network') && (!prefix || entryName === prefix + '.network')) + networkNames.push(entryName); + if (entryName.endsWith('.stacks') && (!prefix || entryName === prefix + '.stacks')) + stacksNames.push(entryName); if (entryName.startsWith('src/') || entryName.includes('src@')) hasSource = true; } - if (!prefixes.length) + if (!traceNames.length) throw new Error('Cannot find .trace file'); this._snapshotStorage = new SnapshotStorage(); - // 3 * ordinals progress increments below. - const total = prefixes.length * 3; + const total = traceNames.length + networkNames.length + stacksNames.length; let done = 0; - for (const prefix of prefixes) { + const contextEntries: ContextEntry[] = []; + + // Load trace files first to learn the trace format version, because network files do not include one. + let version: number | undefined; + for (const traceName of traceNames) { const contextEntry = createEmptyContext(); contextEntry.hasSource = hasSource; const modernizer = new TraceModernizer(contextEntry, this._snapshotStorage); - - const trace = await this._backend.readText(prefix + '.trace') || ''; - modernizer.appendTrace(trace); - unzipProgress?.(++done, total); - - const network = await this._backend.readText(prefix + '.network') || ''; - modernizer.appendTrace(network); + modernizer.appendTrace(await this._backend.readText(traceName) || ''); + version = Math.min(version ?? Number.MAX_SAFE_INTEGER, modernizer.version() ?? Number.MAX_SAFE_INTEGER); unzipProgress?.(++done, total); contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime); @@ -88,24 +93,53 @@ export class TraceLoader { } } - const stacks = await this._backend.readText(prefix + '.stacks'); + contextEntries.push(contextEntry); + } + + for (const networkName of networkNames) { + const contextEntry = createEmptyContext(); + contextEntry.origin = 'library'; + const modernizer = new TraceModernizer(contextEntry, this._snapshotStorage, version === Number.MAX_SAFE_INTEGER ? undefined : version); + modernizer.appendTrace(await this._backend.readText(networkName) || ''); + unzipProgress?.(++done, total); + + // Network files do not include the time origin event, + // so we derive the time origin from the resources instead. + for (const resource of contextEntry.resources) { + // eslint-disable-next-line no-restricted-globals + const wallTime = resource.startedDateTime ? Date.parse(resource.startedDateTime) : NaN; + if (resource._monotonicTime && !isNaN(wallTime)) { + contextEntry.wallTime = wallTime; + contextEntry.monotonicTime = resource._monotonicTime; + break; + } + } + + contextEntries.push(contextEntry); + } + + const callMetadata = new Map(); + for (const stacksName of stacksNames) { + const stacks = await this._backend.readText(stacksName); if (stacks) { - const callMetadata = parseClientSideCallMetadata(JSON.parse(stacks)); - for (const action of contextEntry.actions) - action.stack = action.stack || callMetadata.get(action.callId); + for (const [callId, stack] of parseClientSideCallMetadata(JSON.parse(stacks))) + callMetadata.set(callId, stack); } unzipProgress?.(++done, total); + } + for (const contextEntry of contextEntries) { for (const resource of contextEntry.resources) { if (resource.request.postData?._file) this._resourceToContentType.set(resource.request.postData._file, stripEncodingFromContentType(resource.request.postData.mimeType)); if (resource.response.content?._file) this._resourceToContentType.set(resource.response.content._file, stripEncodingFromContentType(resource.response.content.mimeType)); } - - this.contextEntries.push(contextEntry); } + this.contextEntry = mergeContextEntries(contextEntries); + for (const action of this.contextEntry.actions) + action.stack = action.stack || callMetadata.get(action.callId); this._snapshotStorage.finalize(); } @@ -134,28 +168,124 @@ function stripEncodingFromContentType(contentType: string) { return contentType; } -function createEmptyContext(): ContextEntry { - return { - origin: 'testRunner', - startTime: Number.MAX_SAFE_INTEGER, - wallTime: Number.MAX_SAFE_INTEGER, - monotonicTime: 0, - endTime: 0, - browserName: '', - options: { - deviceScaleFactor: 1, - isMobile: false, - viewport: { width: 1280, height: 800 }, - }, - pages: [], - resources: [], - actions: [], - screenshots: [], - ariaSnapshots: [], - videos: [], - events: [], - errors: [], - stdio: [], - hasSource: false, - }; +function mergeContextEntries(entries: ContextEntry[]): ContextEntry { + const libraryEntries = entries.filter(entry => entry.origin === 'library'); + const testRunnerEntries = entries.filter(entry => entry.origin === 'testRunner'); + + // Align each file with the test runner clock. This updates all the timestamps, + // so it must be done before merging events below. + const timeOrigin = (entry: ContextEntry) => entry.wallTime - entry.monotonicTime; + const runnerEntry = testRunnerEntries.find(entry => entry.monotonicTime); + for (const entry of libraryEntries) { + if (runnerEntry && entry.monotonicTime) + adjustMonotonicTime(entry, timeOrigin(entry) - timeOrigin(runnerEntry)); + } + + const libraryEntry = libraryEntries[0]; + const testRunnerEntry = testRunnerEntries[0]; + const result = createEmptyContext(); + result.origin = libraryEntry ? 'library' : 'testRunner'; + result.browserName = libraryEntry?.browserName || ''; + result.channel = libraryEntry?.channel; + result.platform = libraryEntry?.platform; + result.playwrightVersion = entries.find(entry => entry.playwrightVersion)?.playwrightVersion; + result.sdkLanguage = libraryEntry?.sdkLanguage; + result.testIdAttributeName = libraryEntry?.testIdAttributeName; + result.title = libraryEntry?.title; + result.options = libraryEntry?.options || {}; + result.testTimeout = testRunnerEntry?.testTimeout; + result.annotations = testRunnerEntry?.annotations; + result.hasSource = entries.some(entry => entry.hasSource); + result.hasStepData = !!testRunnerEntry; + result.wallTime = entries.reduce((prev, entry) => Math.min(prev, entry.wallTime), result.wallTime); + result.startTime = entries.reduce((prev, entry) => Math.min(prev, entry.startTime), result.startTime); + result.endTime = entries.reduce((prev, entry) => Math.max(prev, entry.endTime), result.endTime); + result.pages = entries.flatMap(entry => entry.pages); + result.resources = entries.flatMap(entry => entry.resources); + result.actions = mergeActions(entries); + result.screenshots = entries.flatMap(entry => entry.screenshots); + result.ariaSnapshots = entries.flatMap(entry => entry.ariaSnapshots); + result.videos = entries.flatMap(entry => entry.videos); + result.events = entries.flatMap(entry => entry.events); + result.stdio = entries.flatMap(entry => entry.stdio); + result.errors = entries.flatMap(entry => entry.errors); + return result; +} + +let lastTmpStepId = 0; + +function mergeActions(entries: ContextEntry[]): ActionEntry[] { + const libraryEntries = entries.filter(entry => entry.origin === 'library'); + const testRunnerEntries = entries.filter(entry => entry.origin === 'testRunner'); + + // With library-only or test-runner-only traces there is nothing to match. + if (!testRunnerEntries.length || !libraryEntries.length) + return entries.flatMap(entry => entry.actions.map(action => ({ ...action }))); + + const map = new Map(); + for (const entry of libraryEntries) { + for (const action of entry.actions) { + // Never merge stepless events. + map.set(action.stepId || `tmp-step@${++lastTmpStepId}`, { ...action }); + } + } + + const nonPrimaryIdToPrimaryId = new Map(); + for (const entry of testRunnerEntries) { + for (const action of entry.actions) { + const existing = action.stepId && map.get(action.stepId); + if (existing) { + nonPrimaryIdToPrimaryId.set(action.callId, existing.callId); + if (action.error) + existing.error = action.error; + if (action.attachments) + existing.attachments = action.attachments; + if (action.annotations) + existing.annotations = action.annotations; + if (action.parentId) + existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId; + if (action.group) + existing.group = action.group; + // For the events that are present in the test runner context, always take + // their time from the test runner context to preserve client side order. + existing.startTime = action.startTime; + existing.endTime = action.endTime; + continue; + } + if (action.parentId) + action.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId; + map.set(action.stepId || `tmp-step@${++lastTmpStepId}`, { ...action }); + } + } + return [...map.values()]; +} + +function adjustMonotonicTime(entry: ContextEntry, monotonicTimeDelta: number) { + if (!monotonicTimeDelta) + return; + if (entry.startTime !== Number.MAX_SAFE_INTEGER) + entry.startTime += monotonicTimeDelta; + if (entry.endTime) + entry.endTime += monotonicTimeDelta; + entry.monotonicTime += monotonicTimeDelta; + for (const action of entry.actions) { + if (action.startTime) + action.startTime += monotonicTimeDelta; + if (action.endTime) + action.endTime += monotonicTimeDelta; + } + for (const event of entry.events) + event.time += monotonicTimeDelta; + for (const event of entry.stdio) + event.timestamp += monotonicTimeDelta; + for (const page of entry.pages) { + for (const frame of page.screencastFrames) + frame.timestamp += monotonicTimeDelta; + } + for (const video of entry.videos) + video.timestampOrigin += monotonicTimeDelta; + for (const resource of entry.resources) { + if (resource._monotonicTime) + resource._monotonicTime += monotonicTimeDelta; + } } diff --git a/packages/isomorphic/trace/traceModel.ts b/packages/isomorphic/trace/traceModel.ts index eef67a59a6cec..584f505e88a44 100644 --- a/packages/isomorphic/trace/traceModel.ts +++ b/packages/isomorphic/trace/traceModel.ts @@ -93,44 +93,34 @@ export class TraceModel { private _screenshots = new Map(); private _ariaSnapshots = new Map(); - constructor(traceUri: string, contexts: ContextEntry[]) { - const libraryContext = contexts.find(context => context.origin === 'library'); - + constructor(traceUri: string, contextEntry: ContextEntry) { this.traceUri = traceUri; - this.browserName = libraryContext?.browserName || ''; - this.sdkLanguage = libraryContext?.sdkLanguage; - this.channel = libraryContext?.channel; - this.testIdAttributeName = libraryContext?.testIdAttributeName; - this.platform = libraryContext?.platform || ''; - this.playwrightVersion = contexts.find(c => c.playwrightVersion)?.playwrightVersion; - this.title = libraryContext?.title || ''; - this.options = libraryContext?.options || {}; - this.testTimeout = contexts.find(c => c.origin === 'testRunner')?.testTimeout; - this.annotations = contexts.find(c => c.origin === 'testRunner')?.annotations; - // Next call updates all timestamps for all events in library contexts, so it must be done first. - this.actions = mergeActionsAndUpdateTiming(contexts); - this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages)); - this.videos = []; - this.wallTime = contexts.map(c => c.wallTime).reduce((prev, cur) => Math.min(prev || Number.MAX_VALUE, cur!), Number.MAX_VALUE); - this.startTime = contexts.map(c => c.startTime).reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE); - this.endTime = contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE); - 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.errors = ([] as trace.ErrorTraceEvent[]).concat(...contexts.map(c => c.errors)); - this.hasSource = contexts.some(c => c.hasSource); - this.hasStepData = contexts.some(context => context.origin === 'testRunner'); - this.resources = []; - for (let i = 0; i < contexts.length; ++i) { - for (const entry of contexts[i].resources) - this.resources.push({ ...entry, id: `${resourceOwnerRef(entry) ?? i}-${entry.startedDateTime}-${entry.request.url}` }); - } - for (const context of contexts) { - for (const event of context.screenshots || []) - this._screenshots.set(`${event.callId}/${event.phase}`, event); - for (const event of context.ariaSnapshots || []) - this._ariaSnapshots.set(`${event.callId}/${event.phase}`, event); - this.videos.push(...(context.videos || [])); - } + this.browserName = contextEntry.browserName; + this.sdkLanguage = contextEntry.sdkLanguage; + this.channel = contextEntry.channel; + this.testIdAttributeName = contextEntry.testIdAttributeName; + this.platform = contextEntry.platform || ''; + this.playwrightVersion = contextEntry.playwrightVersion; + this.title = contextEntry.title || ''; + this.options = contextEntry.options; + this.testTimeout = contextEntry.testTimeout; + this.annotations = contextEntry.annotations; + this.actions = sortAndLinkActions(contextEntry.actions); + this.pages = contextEntry.pages; + this.videos = contextEntry.videos; + this.wallTime = contextEntry.wallTime; + this.startTime = contextEntry.startTime; + this.endTime = contextEntry.endTime; + this.events = contextEntry.events; + this.stdio = contextEntry.stdio; + this.errors = contextEntry.errors; + this.hasSource = contextEntry.hasSource; + this.hasStepData = contextEntry.hasStepData; + this.resources = contextEntry.resources.map((entry, index) => ({ ...entry, id: `${resourceOwnerRef(entry) ?? index}-${entry.startedDateTime}-${entry.request.url}` })); + for (const event of contextEntry.screenshots) + this._screenshots.set(`${event.callId}/${event.phase}`, event); + for (const event of contextEntry.ariaSnapshots) + this._ariaSnapshots.set(`${event.callId}/${event.phase}`, event); this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, callId: action.callId, traceUri })) ?? []); this.visibleAttachments = this.attachments.filter(attachment => !attachment.name.startsWith('_')); @@ -250,8 +240,8 @@ export class TraceModel { } } -function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) { - const result = mergeActionsAndUpdateTimingSameTrace(contexts); +function sortAndLinkActions(actions: ActionEntry[]) { + const result = actions.slice(); result.sort((a1, a2) => { if (a2.parentId === a1.callId) @@ -278,93 +268,6 @@ function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) { return result; } -let lastTmpStepId = 0; - -function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]): ActionEntry[] { - const map = new Map(); - - const libraryContexts = contexts.filter(context => context.origin === 'library'); - const testRunnerContexts = contexts.filter(context => context.origin === 'testRunner'); - - // With library-only or test-runner-only traces there is nothing to match. - if (!testRunnerContexts.length || !libraryContexts.length) { - return contexts.map(context => { - return context.actions.map(action => ({ ...action })); - }).flat(); - } - - const timeOrigin = (context: ContextEntry) => context.wallTime - context.monotonicTime; - const runnerContext = testRunnerContexts.find(context => context.monotonicTime); - for (const context of libraryContexts) { - if (runnerContext && context.monotonicTime) - adjustMonotonicTime(context, timeOrigin(context) - timeOrigin(runnerContext)); - } - - for (const context of libraryContexts) { - for (const action of context.actions) { - // Never merge stepless events. - map.set(action.stepId || `tmp-step@${++lastTmpStepId}`, { ...action }); - } - } - - const nonPrimaryIdToPrimaryId = new Map(); - for (const context of testRunnerContexts) { - for (const action of context.actions) { - const existing = action.stepId && map.get(action.stepId); - if (existing) { - nonPrimaryIdToPrimaryId.set(action.callId, existing.callId); - if (action.error) - existing.error = action.error; - if (action.attachments) - existing.attachments = action.attachments; - if (action.annotations) - existing.annotations = action.annotations; - if (action.parentId) - existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId; - if (action.group) - existing.group = action.group; - // For the events that are present in the test runner context, always take - // their time from the test runner context to preserve client side order. - existing.startTime = action.startTime; - existing.endTime = action.endTime; - continue; - } - if (action.parentId) - action.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId; - map.set(action.stepId || `tmp-step@${++lastTmpStepId}`, { ...action }); - } - } - return [...map.values()]; -} - -function adjustMonotonicTime(context: ContextEntry, monotonicTimeDelta: number) { - if (!monotonicTimeDelta) - return; - context.startTime += monotonicTimeDelta; - context.endTime += monotonicTimeDelta; - context.monotonicTime += monotonicTimeDelta; - for (const action of context.actions) { - if (action.startTime) - action.startTime += monotonicTimeDelta; - if (action.endTime) - action.endTime += monotonicTimeDelta; - } - for (const event of context.events) - event.time += monotonicTimeDelta; - for (const event of context.stdio) - event.timestamp += monotonicTimeDelta; - for (const page of context.pages) { - for (const frame of page.screencastFrames) - frame.timestamp += monotonicTimeDelta; - } - for (const video of context.videos || []) - video.timestampOrigin += monotonicTimeDelta; - for (const resource of context.resources) { - if (resource._monotonicTime) - resource._monotonicTime += monotonicTimeDelta; - } -} - export function buildActionTree(actions: ActionEntry[]): { rootItem: ActionTreeItem, itemMap: Map } { const itemMap = new Map(); diff --git a/packages/isomorphic/trace/traceModernizer.ts b/packages/isomorphic/trace/traceModernizer.ts index 20047ddb4d73b..c0c282d75c3a5 100644 --- a/packages/isomorphic/trace/traceModernizer.ts +++ b/packages/isomorphic/trace/traceModernizer.ts @@ -49,9 +49,10 @@ export class TraceModernizer { private _consoleObjects = new Map(); private _apiRequestRef: string | undefined; - constructor(contextEntry: ContextEntry, snapshotStorage: SnapshotStorage) { + constructor(contextEntry: ContextEntry, snapshotStorage: SnapshotStorage, version?: number) { this._contextEntry = contextEntry; this._snapshotStorage = snapshotStorage; + this._version = version; } appendTrace(trace: string) { @@ -59,6 +60,10 @@ export class TraceModernizer { this._appendEvent(line); } + version(): number | undefined { + return this._version; + } + actions(): ActionEntry[] { return [...this._actionMap.values()]; } diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 275a09951f377..ff5e2b71c0d62 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -330,6 +330,10 @@ function traceDescriptor(traceDir: string, tracePrefix: string | undefined) { }; for (const name of fs.readdirSync(traceDir)) { + // Skip per-chunk copies of the network file made for zipping, + // they duplicate the main network file. + if (/-pwnetcopy-\d+\.network$/.test(name)) + continue; if (!tracePrefix || name.startsWith(tracePrefix)) result.entries.push({ name, path: toFilePathUrl(path.join(traceDir, name)) }); } diff --git a/packages/playwright-core/src/tools/trace/traceUtils.ts b/packages/playwright-core/src/tools/trace/traceUtils.ts index 4a93dd3f121bd..7170143d7f1fe 100644 --- a/packages/playwright-core/src/tools/trace/traceUtils.ts +++ b/packages/playwright-core/src/tools/trace/traceUtils.ts @@ -90,7 +90,7 @@ export async function loadTrace(): Promise { const backend = new DirTraceLoaderBackend(traceDir); const loader = new TraceLoader(); await loader.load(backend, traceFile); - const model = new TraceModel(traceDir, loader.contextEntries); + const model = new TraceModel(traceDir, loader.contextEntry); return new LoadedTrace(model, loader, buildOrdinalMap(model)); } diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 79d1bc4911923..eb355bdfa69d3 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -205,7 +205,7 @@ async function doFetch(event: FetchEvent): Promise { return errorResponse; if (relativePath === '/contexts') { - return new Response(JSON.stringify(loadedTrace!.traceLoader.contextEntries), { + return new Response(JSON.stringify(loadedTrace!.traceLoader.contextEntry), { status: 200, headers: { 'Content-Type': 'application/json' } }); diff --git a/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx b/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx index 10e98eb7a16c9..b38adc66abaf5 100644 --- a/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx @@ -18,6 +18,7 @@ import * as React from 'react'; import { TraceModel } from '@isomorphic/trace/traceModel'; import './workbenchLoader.css'; import { Workbench } from './workbench'; +import { createEmptyContext } from '@isomorphic/trace/entries'; import type { ContextEntry } from '@isomorphic/trace/entries'; @@ -36,7 +37,7 @@ export const LiveWorkbenchLoader: React.FC<{ traceJson: string }> = ({ traceJson const model = await loadSingleTraceFile(traceJson); setModel(model); } catch { - const model = new TraceModel('', []); + const model = new TraceModel('', createEmptyContext()); setModel(model); } finally { setCounter(counter + 1); @@ -55,6 +56,6 @@ async function loadSingleTraceFile(traceJson: string): Promise { const params = new URLSearchParams(); params.set('trace', traceJson); const response = await fetch(`contexts?${params.toString()}`); - const contextEntries = await response.json() as ContextEntry[]; - return new TraceModel(traceJson, contextEntries); + const contextEntry = await response.json() as ContextEntry; + return new TraceModel(traceJson, contextEntry); } diff --git a/packages/trace-viewer/src/ui/uiModeTraceView.tsx b/packages/trace-viewer/src/ui/uiModeTraceView.tsx index 30b70a84cba44..d5d9ed7cb1bbb 100644 --- a/packages/trace-viewer/src/ui/uiModeTraceView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTraceView.tsx @@ -20,6 +20,8 @@ import '@web/common.css'; import '@web/third_party/vscode/codicon.css'; import type * as reporterTypes from 'playwright/types/testReporter'; import React from 'react'; +import { createEmptyContext } from '@isomorphic/trace/entries'; + import type { ContextEntry } from '@isomorphic/trace/entries'; import type { SourceLocation } from '@isomorphic/trace/traceModel'; import { TraceModel } from '@isomorphic/trace/traceModel'; @@ -75,7 +77,7 @@ export const TraceView: React.FC<{ const model = await loadSingleTraceFile(traceLocation, Date.now()); setModel({ model, isLive: true }); } catch { - const model = new TraceModel('', []); + const model = new TraceModel('', createEmptyContext()); model.errorDescriptors.push(...result.errors.flatMap(error => !!error.message ? [{ message: error.message }] : [])); setModel({ model, isLive: false }); } finally { @@ -115,6 +117,6 @@ async function loadSingleTraceFile(absolutePath: string, timestamp: number): Pro const params = new URLSearchParams(); params.set('trace', traceUri); const response = await fetch(`contexts?${params.toString()}`); - const contextEntries = await response.json() as ContextEntry[]; - return new TraceModel(traceUri, contextEntries); + const contextEntry = await response.json() as ContextEntry; + return new TraceModel(traceUri, contextEntry); } diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index e3718e63368b5..427935a3ac7be 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -15,6 +15,7 @@ */ import * as React from 'react'; +import { createEmptyContext } from '@isomorphic/trace/entries'; import { TraceModel } from '@isomorphic/trace/traceModel'; import './workbenchLoader.css'; import { Workbench } from './workbench'; @@ -134,8 +135,8 @@ export const WorkbenchLoader: React.FunctionComponent<{ setProcessingErrorMessage(error); return error; } - const contextEntries = await response.json(); - const model = new TraceModel(traceURL, contextEntries); + const contextEntry = await response.json(); + const model = new TraceModel(traceURL, contextEntry); setProgress({ done: 0, total: 0 }); setProcessingErrorMessage(null); setModel(model); @@ -245,4 +246,4 @@ export const WorkbenchLoader: React.FunctionComponent<{ ; }; -export const emptyModel = new TraceModel('', []); +export const emptyModel = new TraceModel('', createEmptyContext()); diff --git a/tests/config/utils.ts b/tests/config/utils.ts index eca03a8006092..4d16f16831981 100644 --- a/tests/config/utils.ts +++ b/tests/config/utils.ts @@ -171,7 +171,7 @@ export async function parseTrace(file: string): Promise<{ snapshots: SnapshotSto const backend = new tools.DirTraceLoaderBackend(dir); const loader = new TraceLoader(); await loader.load(backend); - return { model: new TraceModel(dir, loader.contextEntries), snapshots: loader.storage() }; + return { model: new TraceModel(dir, loader.contextEntry), snapshots: loader.storage() }; } export async function parseHar(file: string): Promise> {