Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions packages/isomorphic/trace/traceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ export type SourceModel = {
content: string | undefined;
};

export type ResourceEntry = ResourceSnapshot & { id: string, contextTitle: string };
export type ResourceEntry = ResourceSnapshot & { id: string };

export function resourceOwnerRef(resource: ResourceSnapshot): string | undefined {
return resource.pageref ?? resource._serviceWorkerRef ?? resource._apiRequestRef;
}

export type ActionTreeItem = {
id: string;
Expand Down Expand Up @@ -84,7 +88,7 @@ export class TraceModel {
readonly traceUri: string;
readonly testTimeout?: number;
readonly annotations?: trace.TraceEventAnnotation[];
readonly pagerefToTitle = new Map<string, string>();
readonly resourceOwnerRefToTitle = new Map<string, string>();
private _eventsForAction = new Map<ActionEntry, (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[]>();
private _screenshots = new Map<string, trace.ScreenshotTraceEvent>();
private _ariaSnapshots = new Map<string, trace.AriaSnapshotTraceEvent>();
Expand Down Expand Up @@ -116,12 +120,9 @@ export class TraceModel {
this.hasSource = contexts.some(c => c.hasSource);
this.hasStepData = contexts.some(context => context.origin === 'testRunner');
this.resources = [];
let lastApiContextId = 0;
let lastBrowserContextId = 0;
for (const context of contexts) {
const contextTitle = context.resources.some(resource => resource._apiRequest) ? 'api#' + (++lastApiContextId) : 'browser#' + (++lastBrowserContextId);
for (const entry of context.resources)
this.resources.push({ ...entry, id: `${entry.pageref ?? lastApiContextId}-${entry.startedDateTime}-${entry.request.url}`, contextTitle });
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 || [])
Expand All @@ -133,10 +134,19 @@ export class TraceModel {
this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, callId: action.callId, traceUri })) ?? []);
this.visibleAttachments = this.attachments.filter(attachment => !attachment.name.startsWith('_'));

this.pages.forEach((page, index) => this.pagerefToTitle.set(page.pageId, 'page#' + (index + 1)));
this.pages.forEach((page, index) => this.resourceOwnerRefToTitle.set(page.pageId, 'page#' + (index + 1)));

this.events.sort((a1, a2) => a1.time - a2.time);
this.resources.sort((a1, a2) => a1._monotonicTime! - a2._monotonicTime!);

let serviceWorkerCount = 0;
let apiRequestCount = 0;
for (const resource of this.resources) {
if (resource._serviceWorkerRef && !this.resourceOwnerRefToTitle.has(resource._serviceWorkerRef))
this.resourceOwnerRefToTitle.set(resource._serviceWorkerRef, `service-worker#${++serviceWorkerCount}`);
if (resource._apiRequestRef && !this.resourceOwnerRefToTitle.has(resource._apiRequestRef))
this.resourceOwnerRefToTitle.set(resource._apiRequestRef, `api#${++apiRequestCount}`);
}
this.errorDescriptors = this.hasStepData ? this._errorDescriptorsFromTestRunner() : this._errorDescriptorsFromActions();
this.sources = collectSources(this.actions, this.errorDescriptors);

Expand Down
24 changes: 23 additions & 1 deletion packages/isomorphic/trace/traceModernizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ export class TraceVersionError extends Error {

// 6 => 10/2023 ~1.40
// 7 => 05/2024 ~1.45
const latestVersion: trace.VERSION = 8;
// 9 => 08/2026 ~1.63
const latestVersion: trace.VERSION = 9;

// Ensures distinct api request refs across contexts of the same trace.
let lastApiRequestRefOrdinal = 0;

export class TraceModernizer {
private _contextEntry: ContextEntry;
Expand All @@ -43,6 +47,7 @@ export class TraceModernizer {
private _pageEntries = new Map<string, PageEntry>();
private _jsHandles = new Map<string, { preview: string }>();
private _consoleObjects = new Map<string, { type: string, text: string, location: { url: string, lineNumber: number, columnNumber: number }, args?: { preview: string, value: string }[] }>();
private _apiRequestRef: string | undefined;

constructor(contextEntry: ContextEntry, snapshotStorage: SnapshotStorage) {
this._contextEntry = contextEntry;
Expand Down Expand Up @@ -492,4 +497,21 @@ export class TraceModernizer {
}
return result;
}

_modernize_8_to_9(events: traceV8.TraceEvent[]): trace.TraceEvent[] {
for (const event of events) {
if (event.type !== 'resource-snapshot')
continue;
const snapshot = event.snapshot;
// Older traces marked api requests with a boolean instead of referencing
// their api request context.
if ((snapshot as any)._apiRequest) {
if (!this._apiRequestRef)
this._apiRequestRef = 'api-request-context@' + (++lastApiRequestRefOrdinal);
snapshot._apiRequestRef = this._apiRequestRef;
delete (snapshot as any)._apiRequest;
}
}
return events as trace.TraceEvent[];
}
}
5 changes: 4 additions & 1 deletion packages/playwright-core/src/server/har/harTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export class HarTracer {
if (!this._shouldIncludeEntryWithUrl(event.url.toString()))
return;
const harEntry = createHarEntry(undefined, event.method, event.url, undefined, this._options);
harEntry._apiRequest = true;
harEntry._apiRequestRef = this._context.guid;
if (!this._options.omitCookies)
harEntry.request.cookies = event.cookies;
harEntry.request.headers = Object.entries(event.headers).map(([name, value]) => ({ name, value }));
Expand Down Expand Up @@ -285,6 +285,9 @@ export class HarTracer {

const pageEntry = this._createPageEntryIfNeeded(page);
const harEntry = createHarEntry(pageEntry?.id, request.method(), url, request.frame()?.guid, this._options, request.wallTimeMs());
const serviceWorker = request.serviceWorker();
if (serviceWorker)
harEntry._serviceWorkerRef = serviceWorker.guid;
harEntry._resourceType = request.resourceType();
this._recordRequestHeadersAndCookies(harEntry, request.headers());
harEntry.request.postData = this._postDataForRequest(request, this._options.content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import type { Progress } from '../../progress';
import type * as types from '../../types';
import type { Screencast, ScreencastClient } from '../../screencast';

const version: trace.VERSION = 8;
const version: trace.VERSION = 9;

export type TracerOptions = {
name?: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/tools/trace/traceRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,14 @@ function bytesToString(bytes: number): string {
return gb.toFixed(1) + 'G';
}

function formatRouteStatus(r: { _wasAborted?: boolean, _wasContinued?: boolean, _wasFulfilled?: boolean, _apiRequest?: boolean }): string {
function formatRouteStatus(r: { _wasAborted?: boolean, _wasContinued?: boolean, _wasFulfilled?: boolean, _apiRequestRef?: string }): string {
if (r._wasAborted)
return 'aborted';
if (r._wasContinued)
return 'continued';
if (r._wasFulfilled)
return 'fulfilled';
if (r._apiRequest)
if (r._apiRequestRef)
return 'api';
return '';
}
2 changes: 1 addition & 1 deletion packages/playwright/src/worker/testTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import type EventEmitter from 'events';

export type Attachment = TestInfo['attachments'][0];
export const testTraceEntryName = 'test.trace';
const version: trace.VERSION = 8;
const version: trace.VERSION = 9;
let traceOrdinal = 0;

type TraceFixtureValue = PlaywrightWorkerOptions['trace'] | undefined;
Expand Down
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/ui/consoleTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function useConsoleTabModel(model: TraceModel | undefined, selectedTime:
const { entries } = React.useMemo(() => {
if (!model)
return { entries: [] };
const pageTitle = (id: string | undefined) => (id && model.pagerefToTitle.get(id)) || '';
const pageTitle = (id: string | undefined) => (id && model.resourceOwnerRefToTitle.get(id)) || '';
const entries: ConsoleEntry[] = [];
function addEntry(entry: Omit<ConsoleEntry, 'repeat'>) {
const lastEntry = entries[entries.length - 1];
Expand Down
12 changes: 5 additions & 7 deletions packages/trace-viewer/src/ui/networkTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import './networkTab.css';
import { NetworkResourceDetails, WebSocketResourceDetails } from './networkResourceDetails';
import { bytesToString, msToString } from '@isomorphic/formatUtils';
import { PlaceholderPanel } from './placeholderPanel';
import { resourceOwnerRef } from '@isomorphic/trace/traceModel';
import type { ResourceEntry, TraceModel } from '@isomorphic/trace/traceModel';
import { GridView, type RenderedGridCell } from '@web/components/gridView';
import { SplitView } from '@web/components/splitView';
Expand Down Expand Up @@ -216,13 +217,10 @@ const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell
};

function resourceContextId(model: TraceModel | undefined, resource: ResourceEntry): string {
if (!model)
const ownerRef = resourceOwnerRef(resource);
if (!model || !ownerRef)
return '';
if (resource.pageref)
return model.pagerefToTitle.get(resource.pageref) || '';
if (resource._apiRequest)
return resource.contextTitle;
return '';
return model.resourceOwnerRefToTitle.get(ownerRef) || '';
}

const renderEntry = (resource: ResourceEntry, boundaries: Boundaries, model: TraceModel | undefined): RenderedEntry => {
Expand Down Expand Up @@ -275,7 +273,7 @@ function formatRouteStatus(request: ResourceEntry): string {
return 'continued';
if (request._wasFulfilled)
return 'fulfilled';
if (request._apiRequest)
if (request._apiRequestRef)
return 'api';
return '';
}
Expand Down
3 changes: 2 additions & 1 deletion packages/trace/src/har.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export type Entry = {
_wasAborted?: boolean;
_wasFulfilled?: boolean;
_wasContinued?: boolean;
_apiRequest?: boolean;
_serviceWorkerRef?: string;
_apiRequestRef?: string;
_resourceType?: string;
_webSocketMessages?: WebSocketMessage[];
};
Expand Down
2 changes: 1 addition & 1 deletion packages/trace/src/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export type SerializedError = {
};

// Make sure you add _modernize_N_to_N1(event: any) to traceModernizer.ts.
export type VERSION = 8;
export type VERSION = 9;

export type BrowserContextEventOptions = {
baseURL?: string,
Expand Down
15 changes: 15 additions & 0 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,21 @@ test('should have network requests', async ({ showTraceViewer }) => {
await expect(traceViewer.networkRequests.filter({ hasText: '404GET404text' })).toHaveCSS('background-color', 'rgb(242, 222, 222)');
});

test('should attribute network requests to service workers', async ({ runAndTrace, page, context, server, browserName }) => {
test.skip(browserName !== 'chromium', 'Service worker requests are only reported in Chromium');
const traceViewer = await runAndTrace(async () => {
const [worker] = await Promise.all([
context.waitForEvent('serviceworker'),
page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html'),
]);
await page.evaluate(() => window['activationPromise']);
await worker.evaluate(() => fetch('/one-style.css'));
});
await traceViewer.showNetworkTab();
await expect(traceViewer.networkRequests.filter({ hasText: 'sw.html' })).toContainText(['page#1']);
await expect(traceViewer.networkRequests.filter({ hasText: 'one-style.css' })).toContainText(['service-worker#1']);
});

test('should highlight network request on timeline on hover', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer(traceFile);
await traceViewer.selectAction('Navigate');
Expand Down
3 changes: 2 additions & 1 deletion tests/library/tracing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ test('should record context API request trace independently', async ({ context,
const browserTrace = await parseTraceRaw(browserTracePath);
expect(browserTrace.actions).toContain('Navigate to "/one-style.html"');
expect(browserTrace.actions).not.toContain('POST "/simple.json"');
expect(browserTrace.events.some(event => event.type === 'resource-snapshot' && event.snapshot._apiRequest)).toBe(false);
expect(browserTrace.events.some(event => event.type === 'resource-snapshot' && event.snapshot.request.url.endsWith('/simple.json'))).toBe(false);
expect(browserTrace.events.some(event => event.type === 'resource-snapshot' && event.snapshot.request.url.endsWith('/one-style.html'))).toBe(true);

const apiTrace = await parseTraceRaw(apiTracePath);
Expand All @@ -233,6 +233,7 @@ test('should record context API request trace independently', async ({ context,
const apiAction = apiTrace.actionObjects.find(action => action.class === 'APIRequestContext' && action.method === 'fetch')!;
expect(relativeStack(apiAction, apiTrace.stacks)).toEqual(['tracing.spec.ts']);
expect(apiTrace.events.filter(event => event.type === 'resource-snapshot').map(event => event.snapshot.request.url)).toEqual([apiURL]);
expect(apiTrace.events.filter(event => event.type === 'resource-snapshot').map(event => event.snapshot._apiRequestRef)).toEqual([expect.stringMatching(/^request-context@/)]);
});

test('should collect two traces', async ({ context, page, server }, testInfo) => {
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright-test/playwright.trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ test('should not mixup network files between contexts', async ({ runInlineTest,
expect(apiURLsByTrace.every(urls => urls.length <= 1)).toBe(true);
expect(apiURLsByTrace.flat().sort()).toEqual(apiURLs);
const trace = await parseTrace(tracePath);
expect(trace.model.resources.filter(resource => resource._apiRequest).map(resource => resource.request.url).sort()).toEqual(apiURLs);
expect(trace.model.resources.filter(resource => resource._apiRequestRef).map(resource => resource.request.url).sort()).toEqual(apiURLs);
});

test('should save sources when requested', async ({ runInlineTest }, testInfo) => {
Expand Down
Loading