From 05472f5ef69a440d04ede3e85c94b1a9bc6a5682 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 20 Dec 2024 05:01:16 -0800 Subject: [PATCH] feat: Add time information to Call and Network tabs in Trace Viewer (#33935) --- packages/trace-viewer/src/ui/callTab.tsx | 67 +++++++++++-------- packages/trace-viewer/src/ui/metadataView.tsx | 4 +- .../src/ui/networkResourceDetails.tsx | 14 ++-- packages/trace-viewer/src/ui/networkTab.tsx | 2 +- packages/trace-viewer/src/ui/workbench.tsx | 2 +- tests/library/trace-viewer.spec.ts | 6 +- 6 files changed, 58 insertions(+), 37 deletions(-) diff --git a/packages/trace-viewer/src/ui/callTab.tsx b/packages/trace-viewer/src/ui/callTab.tsx index 1ab3b5b46f4b9..1c78920f86e0f 100644 --- a/packages/trace-viewer/src/ui/callTab.tsx +++ b/packages/trace-viewer/src/ui/callTab.tsx @@ -27,50 +27,63 @@ import type { ActionTraceEventInContext } from './modelUtil'; export const CallTab: React.FunctionComponent<{ action: ActionTraceEventInContext | undefined, + startTimeOffset: number, sdkLanguage: Language | undefined, -}> = ({ action, sdkLanguage }) => { +}> = ({ action, startTimeOffset, sdkLanguage }) => { + // We never need the waitForEventInfo (`info`). + const paramKeys = React.useMemo(() => Object.keys(action?.params ?? {}).filter(name => name !== 'info'), [action]); + if (!action) return ; - const params = { ...action.params }; - // Strip down the waitForEventInfo data, we never need it. - delete params.info; - const paramKeys = Object.keys(params); - const timeMillis = action.startTime + (action.context.wallTime - action.context.startTime); - const wallTime = new Date(timeMillis).toLocaleString(); + + // Calculate execution time relative to the test runner's start time + const startTimeMillis = action.startTime - startTimeOffset; + const startTime = msToString(startTimeMillis); + const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out'; - return
-
{action.apiName}
- {<> -
Time
- {wallTime &&
wall time:{wallTime}
} -
duration:{duration}
- } - { !!paramKeys.length &&
Parameters
} - { - !!paramKeys.length && paramKeys.map((name, index) => renderProperty(propertyToString(action, name, params[name], sdkLanguage), 'param-' + index)) - } - { !!action.result &&
Return value
} - { - !!action.result && Object.keys(action.result).map((name, index) => - renderProperty(propertyToString(action, name, action.result[name], sdkLanguage), 'result-' + index) - ) - } -
; + return ( +
+
{action.apiName}
+ { + <> +
Time
+ + + + } + { + !!paramKeys.length && <> +
Parameters
+ {paramKeys.map(name => renderProperty(propertyToString(action, name, action.params[name], sdkLanguage)))} + + } + { + !!action.result && <> +
Return value
+ {Object.keys(action.result).map(name => + renderProperty(propertyToString(action, name, action.result[name], sdkLanguage)) + )} + + } +
+ ); }; +const DateTimeCallLine: React.FC<{ name: string, value: string }> = ({ name, value }) =>
{name}{value}
; + type Property = { name: string; type: 'string' | 'number' | 'object' | 'locator' | 'handle' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'function'; text: string; }; -function renderProperty(property: Property, key: string) { +function renderProperty(property: Property) { let text = property.text.replace(/\n/g, '↵'); if (property.type === 'string') text = `"${text}"`; return ( -
+
{property.name}:{text} { ['string', 'number', 'object', 'locator'].includes(property.type) && diff --git a/packages/trace-viewer/src/ui/metadataView.tsx b/packages/trace-viewer/src/ui/metadataView.tsx index c1802a4b4d78c..88c2e2bf933fd 100644 --- a/packages/trace-viewer/src/ui/metadataView.tsx +++ b/packages/trace-viewer/src/ui/metadataView.tsx @@ -25,9 +25,11 @@ export const MetadataView: React.FunctionComponent<{ if (!model) return <>; + const wallTime = model.wallTime !== undefined ? new Date(model.wallTime).toLocaleString(undefined, { timeZoneName: 'short' }) : undefined; + return
Time
- {!!model.wallTime &&
start time:{new Date(model.wallTime).toLocaleString()}
} + {!!wallTime &&
start time:{wallTime}
}
duration:{msToString(model.endTime - model.startTime)}
Browser
engine:{model.browserName}
diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index 0c4af4e969b1c..aaa78d17860a0 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -24,12 +24,14 @@ import { generateCurlCommand, generateFetchCall } from '../third_party/devtools' import { CopyToClipboardTextButton } from './copyToClipboard'; import { getAPIRequestCodeGen } from './codegen'; import type { Language } from '@isomorphic/locatorGenerators'; +import { msToString } from '@web/uiUtils'; export const NetworkResourceDetails: React.FunctionComponent<{ resource: ResourceSnapshot; - onClose: () => void; sdkLanguage: Language; -}> = ({ resource, onClose, sdkLanguage }) => { + startTimeOffset: number; + onClose: () => void; +}> = ({ resource, sdkLanguage, startTimeOffset, onClose }) => { const [selectedTab, setSelectedTab] = React.useState('request'); return , + render: () => , }, { id: 'response', @@ -59,7 +61,8 @@ export const NetworkResourceDetails: React.FunctionComponent<{ const RequestTab: React.FunctionComponent<{ resource: ResourceSnapshot; sdkLanguage: Language; -}> = ({ resource, sdkLanguage }) => { + startTimeOffset: number; +}> = ({ resource, sdkLanguage, startTimeOffset }) => { const [requestBody, setRequestBody] = React.useState<{ text: string, mimeType?: string } | null>(null); React.useEffect(() => { @@ -96,6 +99,9 @@ const RequestTab: React.FunctionComponent<{ : null}
Request Headers
{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
+
Time
+
{`Start: ${msToString(startTimeOffset)}`}
+
{`Duration: ${msToString(resource.time)}`}
generateCurlCommand(resource)} /> diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx index 56cf9325b479b..ceaafdcec52cf 100644 --- a/packages/trace-viewer/src/ui/networkTab.tsx +++ b/packages/trace-viewer/src/ui/networkTab.tsx @@ -117,7 +117,7 @@ export const NetworkTab: React.FunctionComponent<{ sidebarIsFirst={true} orientation='horizontal' settingName='networkResourceDetails' - main={ setSelectedEntry(undefined)} sdkLanguage={sdkLanguage} />} + main={ setSelectedEntry(undefined)} />} sidebar={grid} />} ; diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index ad8a099ea4fde..7a14d495c69a6 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -176,7 +176,7 @@ export const Workbench: React.FunctionComponent<{ const callTab: TabbedPaneTabModel = { id: 'call', title: 'Call', - render: () => + render: () => }; const logTab: TabbedPaneTabModel = { id: 'log', diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 3ffba28582d77..53c6bcf6c4497 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -240,7 +240,7 @@ test('should show params and return value', async ({ showTraceViewer }) => { await traceViewer.selectAction('page.evaluate'); await expect(traceViewer.callLines).toHaveText([ /page.evaluate/, - /wall time:[0-9/:,APM ]+/, + /start:[\d\.]+m?s/, /duration:[\d]+ms/, /expression:"\({↵ a↵ }\) => {↵ console\.log\(\'Info\'\);↵ console\.warn\(\'Warning\'\);↵ console/, 'isFunction:true', @@ -251,7 +251,7 @@ test('should show params and return value', async ({ showTraceViewer }) => { await traceViewer.selectAction(`locator('button')`); await expect(traceViewer.callLines).toContainText([ /expect.toHaveText/, - /wall time:[0-9/:,APM ]+/, + /start:[\d\.]+m?s/, /duration:[\d]+ms/, /locator:locator\('button'\)/, /expression:"to.have.text"/, @@ -266,7 +266,7 @@ test('should show null as a param', async ({ showTraceViewer, browserName }) => await traceViewer.selectAction('page.evaluate', 1); await expect(traceViewer.callLines).toHaveText([ /page.evaluate/, - /wall time:[0-9/:,APM ]+/, + /start:[\d\.]+m?s/, /duration:[\d]+ms/, 'expression:"() => 1 + 1"', 'isFunction:true',