Skip to content

Commit

Permalink
feat(trace-viewer): show nework request source id
Browse files Browse the repository at this point in the history
  • Loading branch information
yury-s committed May 14, 2024
1 parent fb319e6 commit 8d982b9
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 8 deletions.
4 changes: 3 additions & 1 deletion packages/trace-viewer/src/ui/modelUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ function indexModel(context: ContextEntry) {
}
for (const event of context.events)
(event as any)[contextSymbol] = context;
for (const resource of context.resources)
(resource as any)[contextSymbol] = context;
}

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

export function context(action: ActionTraceEvent | trace.EventTraceEvent): ContextEntry {
export function context(action: ActionTraceEvent | trace.EventTraceEvent | ResourceSnapshot): ContextEntry {
return (action as any)[contextSymbol];
}

Expand Down
97 changes: 91 additions & 6 deletions packages/trace-viewer/src/ui/networkTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import './networkTab.css';
import { NetworkResourceDetails } from './networkResourceDetails';
import { bytesToString, msToString } from '@web/uiUtils';
import { PlaceholderPanel } from './placeholderPanel';
import type { MultiTraceModel } from './modelUtil';
import { context, type MultiTraceModel } from './modelUtil';
import { GridView, type RenderedGridCell } from '@web/components/gridView';
import { SplitView } from '@web/components/splitView';

Expand All @@ -39,6 +39,7 @@ type RenderedEntry = {
start: number,
route: string,
resource: Entry,
contextId: string,
};
type ColumnName = keyof RenderedEntry;
type Sorting = { by: ColumnName, negate: boolean};
Expand All @@ -60,13 +61,15 @@ export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedT
export const NetworkTab: React.FunctionComponent<{
boundaries: Boundaries,
networkModel: NetworkTabModel,
model?: MultiTraceModel,
onEntryHovered: (entry: Entry | undefined) => void,
}> = ({ boundaries, networkModel, onEntryHovered }) => {
}> = ({ boundaries, networkModel, model, onEntryHovered }) => {
const [sorting, setSorting] = React.useState<Sorting | undefined>(undefined);
const [selectedEntry, setSelectedEntry] = React.useState<RenderedEntry | undefined>(undefined);

const { renderedEntries } = React.useMemo(() => {
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries));
const generator = ContextIdGenerator.forModel(model);
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries, generator));
if (sorting)
sort(renderedEntries, sorting);
return { renderedEntries };
Expand All @@ -81,7 +84,7 @@ export const NetworkTab: React.FunctionComponent<{
selectedItem={selectedEntry}
onSelected={item => setSelectedEntry(item)}
onHighlighted={item => onEntryHovered(item?.resource)}
columns={selectedEntry ? ['name'] : ['name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route']}
columns={visibleColumns(!!selectedEntry, renderedEntries)}
columnTitle={columnTitle}
columnWidth={columnWidth}
isError={item => item.status.code >= 400}
Expand All @@ -100,6 +103,8 @@ export const NetworkTab: React.FunctionComponent<{
};

const columnTitle = (column: ColumnName) => {
if (column === 'contextId')
return 'Source';
if (column === 'name')
return 'Name';
if (column === 'method')
Expand Down Expand Up @@ -131,7 +136,23 @@ const columnWidth = (column: ColumnName) => {
return 100;
};

function visibleColumns(entrySelected: boolean, renderedEntries: RenderedEntry[]): (keyof RenderedEntry)[] {
if (entrySelected)
return ['name'];
const columns: (keyof RenderedEntry)[] = [];
if (hasMultipleContexts(renderedEntries))
columns.push('contextId');
columns.push('name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route');
return columns;
}

const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell => {
if (column === 'contextId') {
return {
body: entry.contextId,
title: entry.name.url,
};
}
if (column === 'name') {
return {
body: entry.name.name,
Expand Down Expand Up @@ -159,7 +180,67 @@ const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell
return { body: '' };
};

const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry => {
class ContextIdGenerator {
private _pagerefToShortId = new Map<string, string>();
private _lastPageId = 0;
private _lastApiRequestContextId = 0;
private static _apiRequestContextIdSymbol = Symbol('apiRequestContextId');
private static _contextIdGeneratorSymbol = Symbol('contextIdGenerator');

static forModel(model?: MultiTraceModel): ContextIdGenerator {
if (!model)
return new ContextIdGenerator();
let generator = (model as any)[ContextIdGenerator._contextIdGeneratorSymbol];
if (!generator) {
generator = new ContextIdGenerator();
(model as any)[ContextIdGenerator._contextIdGeneratorSymbol] = generator;
}
return generator;
}

contextId(resource: Entry): string {
if (resource.pageref)
return this._pageId(resource.pageref);
else if (resource._apiRequest)
return this._apiRequestContextId(resource);
return '';
}

private _pageId(pageref: string): string {
let shortId = this._pagerefToShortId.get(pageref);
if (!shortId) {
++this._lastPageId;
shortId = 'page@' + this._lastPageId;
this._pagerefToShortId.set(pageref, shortId);
}
return shortId;
}

private _apiRequestContextId(resource: Entry): string {
const contextEntry = context(resource);
if (!contextEntry)
return '';
let contextId = (contextEntry as any)[ContextIdGenerator._apiRequestContextIdSymbol];
if (!contextId) {
++this._lastApiRequestContextId;
contextId = 'api@' + this._lastApiRequestContextId;
(contextEntry as any)[ContextIdGenerator._apiRequestContextIdSymbol] = contextId;
}
return contextId;
}
}

function hasMultipleContexts(renderedEntries: RenderedEntry[]): boolean {
const contextIds = new Set<string>();
for (const entry of renderedEntries) {
contextIds.add(entry.contextId);
if (contextIds.size > 1)
return true;
}
return false;
}

const renderEntry = (resource: Entry, boundaries: Boundaries, contextIdGenerator: ContextIdGenerator): RenderedEntry => {
const routeStatus = formatRouteStatus(resource);
let resourceName: string;
try {
Expand All @@ -184,7 +265,8 @@ const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry =>
size: resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize,
start: resource._monotonicTime! - boundaries.minimum,
route: routeStatus,
resource
resource,
contextId: contextIdGenerator.contextId(resource),
};
};

Expand Down Expand Up @@ -249,4 +331,7 @@ function comparator(sortBy: ColumnName) {
return a.route.localeCompare(b.route);
};
}

if (sortBy === 'contextId')
return (a: RenderedEntry, b: RenderedEntry) => a.contextId.localeCompare(b.contextId);
}
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/ui/workbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export const Workbench: React.FunctionComponent<{
id: 'network',
title: 'Network',
count: networkModel.resources.length,
render: () => <NetworkTab boundaries={boundaries} networkModel={networkModel} onEntryHovered={setHighlightedEntry}/>
render: () => <NetworkTab boundaries={boundaries} networkModel={networkModel} model={model} onEntryHovered={setHighlightedEntry}/>
};
const attachmentsTab: TabbedPaneTabModel = {
id: 'attachments',
Expand Down
21 changes: 21 additions & 0 deletions tests/playwright-test/ui-mode-trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,24 @@ test('should reveal errors in the sourcetab', async ({ runUITest }) => {
await page.getByText('a.spec.ts:4', { exact: true }).click();
await expect(page.locator('.source-line-running')).toContainText(`throw new Error('Oh my');`);
});

test('should show request source context id', async ({ runUITest, server }) => {
const { page } = await runUITest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({ page, context, request }) => {
await page.goto('${server.EMPTY_PAGE}');
const page2 = await context.newPage();
await page2.goto('${server.EMPTY_PAGE}');
await request.get('${server.EMPTY_PAGE}');
});
`,
});

await page.getByText('pass').dblclick();
await page.getByText('Network', { exact: true }).click();
await expect(page.locator('span').filter({ hasText: 'Source' })).toBeVisible();
await expect(page.getByText('page@1')).toBeVisible();
await expect(page.getByText('page@2')).toBeVisible();
await expect(page.getByText('api@1')).toBeVisible();
});

0 comments on commit 8d982b9

Please sign in to comment.