Skip to content

Commit 2734a05

Browse files
authored
feat(trace-viewer): show nework request source id (#30810)
<img width="1392" alt="image" src="https://github.com/microsoft/playwright/assets/9798949/dcfd4d71-4a41-48ac-9f24-2996200f966a"> Fixes #28903
1 parent 89cdf3d commit 2734a05

File tree

3 files changed

+109
-8
lines changed

3 files changed

+109
-8
lines changed

packages/trace-viewer/src/ui/modelUtil.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ function indexModel(context: ContextEntry) {
157157
}
158158
for (const event of context.events)
159159
(event as any)[contextSymbol] = context;
160+
for (const resource of context.resources)
161+
(resource as any)[contextSymbol] = context;
160162
}
161163

162164
function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) {
@@ -330,7 +332,7 @@ export function idForAction(action: ActionTraceEvent) {
330332
return `${action.pageId || 'none'}:${action.callId}`;
331333
}
332334

333-
export function context(action: ActionTraceEvent | trace.EventTraceEvent): ContextEntry {
335+
export function context(action: ActionTraceEvent | trace.EventTraceEvent | ResourceSnapshot): ContextEntry {
334336
return (action as any)[contextSymbol];
335337
}
336338

packages/trace-viewer/src/ui/networkTab.tsx

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ import './networkTab.css';
2121
import { NetworkResourceDetails } from './networkResourceDetails';
2222
import { bytesToString, msToString } from '@web/uiUtils';
2323
import { PlaceholderPanel } from './placeholderPanel';
24-
import type { MultiTraceModel } from './modelUtil';
24+
import { context, type MultiTraceModel } from './modelUtil';
2525
import { GridView, type RenderedGridCell } from '@web/components/gridView';
2626
import { SplitView } from '@web/components/splitView';
27+
import type { ContextEntry } from '../entries';
2728

2829
type NetworkTabModel = {
2930
resources: Entry[],
31+
contextIdMap: ContextIdMap,
3032
};
3133

3234
type RenderedEntry = {
@@ -39,6 +41,7 @@ type RenderedEntry = {
3941
start: number,
4042
route: string,
4143
resource: Entry,
44+
contextId: string,
4245
};
4346
type ColumnName = keyof RenderedEntry;
4447
type Sorting = { by: ColumnName, negate: boolean};
@@ -54,7 +57,8 @@ export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedT
5457
});
5558
return filtered;
5659
}, [model, selectedTime]);
57-
return { resources };
60+
const contextIdMap = React.useMemo(() => new ContextIdMap(model), [model]);
61+
return { resources, contextIdMap };
5862
}
5963

6064
export const NetworkTab: React.FunctionComponent<{
@@ -66,11 +70,11 @@ export const NetworkTab: React.FunctionComponent<{
6670
const [selectedEntry, setSelectedEntry] = React.useState<RenderedEntry | undefined>(undefined);
6771

6872
const { renderedEntries } = React.useMemo(() => {
69-
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries));
73+
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries, networkModel.contextIdMap));
7074
if (sorting)
7175
sort(renderedEntries, sorting);
7276
return { renderedEntries };
73-
}, [networkModel.resources, sorting, boundaries]);
77+
}, [networkModel.resources, networkModel.contextIdMap, sorting, boundaries]);
7478

7579
if (!networkModel.resources.length)
7680
return <PlaceholderPanel text='No network calls' />;
@@ -81,7 +85,7 @@ export const NetworkTab: React.FunctionComponent<{
8185
selectedItem={selectedEntry}
8286
onSelected={item => setSelectedEntry(item)}
8387
onHighlighted={item => onEntryHovered(item?.resource)}
84-
columns={selectedEntry ? ['name'] : ['name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route']}
88+
columns={visibleColumns(!!selectedEntry, renderedEntries)}
8589
columnTitle={columnTitle}
8690
columnWidth={columnWidth}
8791
isError={item => item.status.code >= 400}
@@ -100,6 +104,8 @@ export const NetworkTab: React.FunctionComponent<{
100104
};
101105

102106
const columnTitle = (column: ColumnName) => {
107+
if (column === 'contextId')
108+
return 'Source';
103109
if (column === 'name')
104110
return 'Name';
105111
if (column === 'method')
@@ -128,10 +134,28 @@ const columnWidth = (column: ColumnName) => {
128134
return 60;
129135
if (column === 'contentType')
130136
return 200;
137+
if (column === 'contextId')
138+
return 60;
131139
return 100;
132140
};
133141

142+
function visibleColumns(entrySelected: boolean, renderedEntries: RenderedEntry[]): (keyof RenderedEntry)[] {
143+
if (entrySelected)
144+
return ['name'];
145+
const columns: (keyof RenderedEntry)[] = [];
146+
if (hasMultipleContexts(renderedEntries))
147+
columns.push('contextId');
148+
columns.push('name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route');
149+
return columns;
150+
}
151+
134152
const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell => {
153+
if (column === 'contextId') {
154+
return {
155+
body: entry.contextId,
156+
title: entry.name.url,
157+
};
158+
}
135159
if (column === 'name') {
136160
return {
137161
body: entry.name.name,
@@ -159,7 +183,57 @@ const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell
159183
return { body: '' };
160184
};
161185

162-
const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry => {
186+
class ContextIdMap {
187+
private _pagerefToShortId = new Map<string, string>();
188+
private _contextToId = new Map<ContextEntry, string>();
189+
private _lastPageId = 0;
190+
private _lastApiRequestContextId = 0;
191+
192+
constructor(model: MultiTraceModel | undefined) {}
193+
194+
contextId(resource: Entry): string {
195+
if (resource.pageref)
196+
return this._pageId(resource.pageref);
197+
else if (resource._apiRequest)
198+
return this._apiRequestContextId(resource);
199+
return '';
200+
}
201+
202+
private _pageId(pageref: string): string {
203+
let shortId = this._pagerefToShortId.get(pageref);
204+
if (!shortId) {
205+
++this._lastPageId;
206+
shortId = 'page#' + this._lastPageId;
207+
this._pagerefToShortId.set(pageref, shortId);
208+
}
209+
return shortId;
210+
}
211+
212+
private _apiRequestContextId(resource: Entry): string {
213+
const contextEntry = context(resource);
214+
if (!contextEntry)
215+
return '';
216+
let contextId = this._contextToId.get(contextEntry);
217+
if (!contextId) {
218+
++this._lastApiRequestContextId;
219+
contextId = 'api#' + this._lastApiRequestContextId;
220+
this._contextToId.set(contextEntry, contextId);
221+
}
222+
return contextId;
223+
}
224+
}
225+
226+
function hasMultipleContexts(renderedEntries: RenderedEntry[]): boolean {
227+
const contextIds = new Set<string>();
228+
for (const entry of renderedEntries) {
229+
contextIds.add(entry.contextId);
230+
if (contextIds.size > 1)
231+
return true;
232+
}
233+
return false;
234+
}
235+
236+
const renderEntry = (resource: Entry, boundaries: Boundaries, contextIdGenerator: ContextIdMap): RenderedEntry => {
163237
const routeStatus = formatRouteStatus(resource);
164238
let resourceName: string;
165239
try {
@@ -184,7 +258,8 @@ const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry =>
184258
size: resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize,
185259
start: resource._monotonicTime! - boundaries.minimum,
186260
route: routeStatus,
187-
resource
261+
resource,
262+
contextId: contextIdGenerator.contextId(resource),
188263
};
189264
};
190265

@@ -249,4 +324,7 @@ function comparator(sortBy: ColumnName) {
249324
return a.route.localeCompare(b.route);
250325
};
251326
}
327+
328+
if (sortBy === 'contextId')
329+
return (a: RenderedEntry, b: RenderedEntry) => a.contextId.localeCompare(b.contextId);
252330
}

tests/playwright-test/ui-mode-trace.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,24 @@ test('should reveal errors in the sourcetab', async ({ runUITest }) => {
285285
await page.getByText('a.spec.ts:4', { exact: true }).click();
286286
await expect(page.locator('.source-line-running')).toContainText(`throw new Error('Oh my');`);
287287
});
288+
289+
test('should show request source context id', async ({ runUITest, server }) => {
290+
const { page } = await runUITest({
291+
'a.spec.ts': `
292+
import { test, expect } from '@playwright/test';
293+
test('pass', async ({ page, context, request }) => {
294+
await page.goto('${server.EMPTY_PAGE}');
295+
const page2 = await context.newPage();
296+
await page2.goto('${server.EMPTY_PAGE}');
297+
await request.get('${server.EMPTY_PAGE}');
298+
});
299+
`,
300+
});
301+
302+
await page.getByText('pass').dblclick();
303+
await page.getByText('Network', { exact: true }).click();
304+
await expect(page.locator('span').filter({ hasText: 'Source' })).toBeVisible();
305+
await expect(page.getByText('page#1')).toBeVisible();
306+
await expect(page.getByText('page#2')).toBeVisible();
307+
await expect(page.getByText('api#1')).toBeVisible();
308+
});

0 commit comments

Comments
 (0)