fix: miscellaneous improvements for tracing UI (#23558)

- feat(tracing): mark API requests with "API" label
- feat(tracing): do not attribute any resources to `route.` API calls;
  otherwise, network traffic might get inside the `route.` actions.
- fix(tracing): map actionIds from primary contexts to actionIds from
  non-primary contexts
- fix(tracing): show leading `/` in URL path in network panel

This is a result of a pair-programming session with @pavelfeldman
This commit is contained in:
Andrey Lushnikov 2023-06-06 17:38:44 -07:00 committed by GitHub
parent 0f4090472c
commit 2e327c9d0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 28 additions and 9 deletions

View file

@ -196,6 +196,7 @@ export class HarTracer {
if (!this._shouldIncludeEntryWithUrl(event.url.toString())) if (!this._shouldIncludeEntryWithUrl(event.url.toString()))
return; return;
const harEntry = createHarEntry(event.method, event.url, undefined, this._options); const harEntry = createHarEntry(event.method, event.url, undefined, this._options);
harEntry._apiRequest = true;
if (!this._options.omitCookies) if (!this._options.omitCookies)
harEntry.request.cookies = event.cookies; harEntry.request.cookies = event.cookies;
harEntry.request.headers = Object.entries(event.headers).map(([name, value]) => ({ name, value })); harEntry.request.headers = Object.entries(event.headers).map(([name, value]) => ({ name, value }));

View file

@ -86,7 +86,13 @@ function indexModel(context: ContextEntry) {
for (let i = 0; i < context.actions.length; ++i) { for (let i = 0; i < context.actions.length; ++i) {
const action = context.actions[i] as any; const action = context.actions[i] as any;
action[contextSymbol] = context; action[contextSymbol] = context;
action[nextInContextSymbol] = context.actions[i + 1]; }
let lastNonRouteAction = undefined;
for (let i = context.actions.length - 1; i >= 0; i--) {
const action = context.actions[i] as any;
action[nextInContextSymbol] = lastNonRouteAction;
if (!action.apiName.includes('route.'))
lastNonRouteAction = action;
} }
for (const event of context.events) for (const event of context.events)
(event as any)[contextSymbol] = context; (event as any)[contextSymbol] = context;
@ -109,6 +115,7 @@ function mergeActions(contexts: ContextEntry[]) {
offset = context.actions[0].startTime - context.actions[0].wallTime; offset = context.actions[0].startTime - context.actions[0].wallTime;
} }
const nonPrimaryIdToPrimaryId = new Map<string, string>();
for (const context of nonPrimaryContexts) { for (const context of nonPrimaryContexts) {
for (const action of context.actions) { for (const action of context.actions) {
if (offset) { if (offset) {
@ -122,12 +129,13 @@ function mergeActions(contexts: ContextEntry[]) {
const key = `${action.apiName}@${action.wallTime}`; const key = `${action.apiName}@${action.wallTime}`;
const existing = map.get(key); const existing = map.get(key);
if (existing && existing.apiName === action.apiName) { if (existing && existing.apiName === action.apiName) {
nonPrimaryIdToPrimaryId.set(action.callId, existing.callId);
if (action.error) if (action.error)
existing.error = action.error; existing.error = action.error;
if (action.attachments) if (action.attachments)
existing.attachments = action.attachments; existing.attachments = action.attachments;
if (action.parentId) if (action.parentId)
existing.parentId = action.parentId; existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId;
continue; continue;
} }
map.set(key, { ...action, context }); map.set(key, { ...action, context });

View file

@ -39,6 +39,8 @@
border-radius: 4px; border-radius: 4px;
margin: 2px; margin: 2px;
line-height: 20px; line-height: 20px;
min-width: 30px;
text-align: center;
} }
.network-request-title-status.status-failure { .network-request-title-status.status-failure {
@ -51,6 +53,11 @@
background-color: var(--vscode-statusBar-background); background-color: var(--vscode-statusBar-background);
} }
.network-request-title-status.status-route.api {
color: var(--vscode-statusBar-foreground);
background-color: var(--vscode-statusBarItem-remoteBackground);
}
.network-request-title-url { .network-request-title-url {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View file

@ -69,7 +69,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
const routeStatus = formatRouteStatus(resource); const routeStatus = formatRouteStatus(resource);
const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type'); const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type');
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : ''; const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/') + 1); const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/'));
let contentType = resource.response.content.mimeType; let contentType = resource.response.content.mimeType;
const charset = contentType.match(/^(.*);\s*charset=.*$/); const charset = contentType.match(/^(.*);\s*charset=.*$/);
if (charset) if (charset)
@ -79,7 +79,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
const renderTitle = React.useCallback(() => { const renderTitle = React.useCallback(() => {
return <div className='network-request-title'> return <div className='network-request-title'>
{routeStatus && <div className={'network-request-title-status status-route'}>{routeStatus}</div> } {routeStatus && <div className={`network-request-title-status status-route ${routeStatus}`}>{routeStatus}</div> }
{resource.response._failureText && <div className={'network-request-title-status status-failure'}>{resource.response._failureText}</div>} {resource.response._failureText && <div className={'network-request-title-status status-failure'}>{resource.response._failureText}</div>}
{!resource.response._failureText && <div className={'network-request-title-status ' + formatStatus(resource.response.status)}>{resource.response.status}</div>} {!resource.response._failureText && <div className={'network-request-title-status ' + formatStatus(resource.response.status)}>{resource.response.status}</div>}
<div className='network-request-title-status'>{resource.request.method}</div> <div className='network-request-title-status'>{resource.request.method}</div>
@ -147,5 +147,7 @@ function formatRouteStatus(request: Entry): string {
return 'continued'; return 'continued';
if (request._wasFulfilled) if (request._wasFulfilled)
return 'fulfilled'; return 'fulfilled';
if (request._apiRequest)
return 'api';
return ''; return '';
} }

View file

@ -71,6 +71,7 @@ export type Entry = {
_wasAborted?: boolean; _wasAborted?: boolean;
_wasFulfilled?: boolean; _wasFulfilled?: boolean;
_wasContinued?: boolean; _wasContinued?: boolean;
_apiRequest?: boolean;
}; };
export type Request = { export type Request = {

View file

@ -216,9 +216,9 @@ test('should have network requests', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('http://localhost'); await traceViewer.selectAction('http://localhost');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await expect(traceViewer.networkRequests).toContainText([/200GETframe.htmltext\/html/]); await expect(traceViewer.networkRequests).toContainText([/200GET\/frame.htmltext\/html/]);
await expect(traceViewer.networkRequests).toContainText([/200GETstyle.csstext\/css/]); await expect(traceViewer.networkRequests).toContainText([/200GET\/style.csstext\/css/]);
await expect(traceViewer.networkRequests).toContainText([/200GETscript.jsapplication\/javascript/]); await expect(traceViewer.networkRequests).toContainText([/200GET\/script.jsapplication\/javascript/]);
}); });
test('should have network request overrides', async ({ page, server, runAndTrace }) => { test('should have network request overrides', async ({ page, server, runAndTrace }) => {
@ -228,7 +228,7 @@ test('should have network request overrides', async ({ page, server, runAndTrace
}); });
await traceViewer.selectAction('http://localhost'); await traceViewer.selectAction('http://localhost');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await expect(traceViewer.networkRequests).toContainText([/200GETframe.htmltext\/html/]); await expect(traceViewer.networkRequests).toContainText([/200GET\/frame.htmltext\/html/]);
await expect(traceViewer.networkRequests).toContainText([/abort.*style.cssx-unknown/]); await expect(traceViewer.networkRequests).toContainText([/abort.*style.cssx-unknown/]);
}); });
@ -239,7 +239,7 @@ test('should have network request overrides 2', async ({ page, server, runAndTra
}); });
await traceViewer.selectAction('http://localhost'); await traceViewer.selectAction('http://localhost');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await expect(traceViewer.networkRequests).toContainText([/200GETframe.htmltext\/html/]); await expect(traceViewer.networkRequests).toContainText([/200GET\/frame.htmltext\/html/]);
await expect(traceViewer.networkRequests).toContainText([/continue.*script.jsapplication\/javascript/]); await expect(traceViewer.networkRequests).toContainText([/continue.*script.jsapplication\/javascript/]);
}); });