From fd31f5bc50bd5c290079d976dadb18a48bd075af Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 30 Aug 2023 12:40:46 -0700 Subject: [PATCH] chore: fix har date types (#26783) --- .../src/server/har/harTracer.ts | 24 +++-- .../src/server/trace/recorder/tracing.ts | 2 + packages/trace-viewer/src/ui/networkTab.css | 24 +++-- packages/trace-viewer/src/ui/networkTab.tsx | 88 ++++++++++--------- packages/trace-viewer/src/ui/timeline.tsx | 11 ++- packages/trace-viewer/src/ui/workbench.tsx | 2 +- packages/trace/src/har.ts | 6 +- 7 files changed, 95 insertions(+), 62 deletions(-) diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index 152f793d1f..c1907fbd3f 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -127,8 +127,9 @@ export class HarTracer { return; let pageEntry = this._pageEntries.get(page); if (!pageEntry) { + const date = new Date(); pageEntry = { - startedDateTime: new Date(), + startedDateTime: date.toISOString(), id: page.guid, title: '', pageTimings: this._options.omitTiming ? {} : { @@ -136,6 +137,7 @@ export class HarTracer { onLoad: -1, }, }; + (pageEntry as any)[startedDateSymbol] = date; page.mainFrame().on(Frame.Events.AddLifecycle, (event: LifecycleEvent) => { if (event === 'load') @@ -221,7 +223,7 @@ export class HarTracer { harEntry.response.cookies = this._options.omitCookies ? [] : event.cookies.map(c => { return { ...c, - expires: c.expires === -1 ? undefined : new Date(c.expires) + expires: c.expires === -1 ? undefined : new Date(c.expires).toISOString() }; }); @@ -456,9 +458,10 @@ export class HarTracer { }; if (!this._options.omitTiming) { + const startDateTime = pageEntry ? ((pageEntry as any)[startedDateSymbol] as Date).valueOf() : 0; const timing = response.timing(); - if (pageEntry && pageEntry.startedDateTime.valueOf() > timing.startTime) - pageEntry.startedDateTime = new Date(timing.startTime); + if (pageEntry && startDateTime > timing.startTime) + pageEntry.startedDateTime = new Date(timing.startTime).toISOString(); const dns = timing.domainLookupEnd !== -1 ? helper.millisToRoundishMillis(timing.domainLookupEnd - timing.domainLookupStart) : -1; const connect = timing.connectEnd !== -1 ? helper.millisToRoundishMillis(timing.connectEnd - timing.connectStart) : -1; const ssl = timing.connectEnd !== -1 ? helper.millisToRoundishMillis(timing.connectEnd - timing.secureConnectionStart) : -1; @@ -535,12 +538,13 @@ export class HarTracer { }; if (!this._options.omitTiming) { for (const pageEntry of log.pages || []) { + const startDateTime = ((pageEntry as any)[startedDateSymbol] as Date).valueOf(); if (typeof pageEntry.pageTimings.onContentLoad === 'number' && pageEntry.pageTimings.onContentLoad >= 0) - pageEntry.pageTimings.onContentLoad -= pageEntry.startedDateTime.valueOf(); + pageEntry.pageTimings.onContentLoad -= startDateTime; else pageEntry.pageTimings.onContentLoad = -1; if (typeof pageEntry.pageTimings.onLoad === 'number' && pageEntry.pageTimings.onLoad >= 0) - pageEntry.pageTimings.onLoad -= pageEntry.startedDateTime.valueOf(); + pageEntry.pageTimings.onLoad -= startDateTime; else pageEntry.pageTimings.onLoad = -1; } @@ -597,7 +601,7 @@ function createHarEntry(method: string, url: URL, frameref: string | undefined, const harEntry: har.Entry = { _frameref: options.includeTraceInfo ? frameref : undefined, _monotonicTime: options.includeTraceInfo ? monotonicTime() : undefined, - startedDateTime: new Date(), + startedDateTime: new Date().toISOString(), time: -1, request: { method: method, @@ -654,11 +658,11 @@ function parseCookie(c: string): har.Cookie { if (name === 'Domain') cookie.domain = value; if (name === 'Expires') - cookie.expires = new Date(value); + cookie.expires = new Date(value).toISOString(); if (name === 'HttpOnly') cookie.httpOnly = true; if (name === 'Max-Age') - cookie.expires = new Date(Date.now() + (+value) * 1000); + cookie.expires = new Date(Date.now() + (+value) * 1000).toISOString(); if (name === 'Path') cookie.path = value; if (name === 'SameSite') @@ -668,3 +672,5 @@ function parseCookie(c: string): har.Cookie { } return cookie; } + +const startedDateSymbol = Symbol('startedDate'); \ No newline at end of file diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index ae6bba50ad..7b20832342 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -493,6 +493,8 @@ function visitTraceEvent(object: any, sha1s: Set): any { return object.map(o => visitTraceEvent(o, sha1s)); if (object instanceof Buffer) return undefined; + if (object instanceof Date) + return object; if (typeof object === 'object') { const result: any = {}; for (const key in object) { diff --git a/packages/trace-viewer/src/ui/networkTab.css b/packages/trace-viewer/src/ui/networkTab.css index 9bd7eaa4ac..b714b44769 100644 --- a/packages/trace-viewer/src/ui/networkTab.css +++ b/packages/trace-viewer/src/ui/networkTab.css @@ -29,12 +29,18 @@ background-color: var(--vscode-statusBarItem-remoteBackground); } +.network-request-start { + flex: 0 0 65px; + justify-content: right; + padding-right: 10px; +} + .network-request-status { - flex: 0 0 70px; + flex: 0 0 65px; } .network-request-method { - flex: 0 0 70px; + flex: 0 0 65px; } .network-request-file { @@ -49,7 +55,7 @@ } .network-request-content-type, -.network-request-time, +.network-request-duration, .network-request-route, .network-request-size { overflow: hidden; @@ -81,6 +87,14 @@ display: flex; align-items: center; white-space: nowrap; + height: 100%; +} + +.network-request-header.filter-start.positive .network-request-start .codicon-triangle-down { + display: initial !important; +} +.network-request-header.filter-start.negative .network-request-start .codicon-triangle-up { + display: initial !important; } .network-request-header.filter-status.positive .network-request-status .codicon-triangle-down { @@ -111,10 +125,10 @@ display: initial !important; } -.network-request-header.filter-time.positive .network-request-time .codicon-triangle-down { +.network-request-header.filter-duration.positive .network-request-duration .codicon-triangle-down { display: initial !important; } -.network-request-header.filter-time.negative .network-request-time .codicon-triangle-up { +.network-request-header.filter-duration.negative .network-request-duration .codicon-triangle-up { display: initial !important; } diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx index 8e366d3a0e..0d714d3495 100644 --- a/packages/trace-viewer/src/ui/networkTab.tsx +++ b/packages/trace-viewer/src/ui/networkTab.tsx @@ -25,16 +25,17 @@ import { bytesToString, msToString } from '@web/uiUtils'; const NetworkListView = ListView; -type Filter = 'status' | 'method' | 'file' | 'time' | 'size' | 'content-type'; +type SortBy = 'start' | 'status' | 'method' | 'file' | 'duration' | 'size' | 'content-type'; +type Sorting = { by: SortBy, negate: boolean}; export const NetworkTab: React.FunctionComponent<{ model: modelUtil.MultiTraceModel | undefined, + boundaries: Boundaries, selectedTime: Boundaries | undefined, onEntryHovered: (entry: Entry | undefined) => void, -}> = ({ model, selectedTime, onEntryHovered }) => { +}> = ({ model, boundaries, selectedTime, onEntryHovered }) => { const [resource, setResource] = React.useState(); - const [filter, setFilter] = React.useState(undefined); - const [negateFilter, setNegateFilter] = React.useState(false); + const [sorting, setSorting] = React.useState(undefined); const resources = React.useMemo(() => { const resources = model?.resources || []; @@ -43,27 +44,22 @@ export const NetworkTab: React.FunctionComponent<{ return true; return !!resource._monotonicTime && (resource._monotonicTime >= selectedTime.minimum && resource._monotonicTime <= selectedTime.maximum); }); - if (filter) - sort(filtered, filter, negateFilter); + if (sorting) + sort(filtered, sorting); return filtered; - }, [filter, model, negateFilter, selectedTime]); + }, [sorting, model, selectedTime]); - const toggleFilter = React.useCallback((f: Filter) => { - if (filter === f) { - setNegateFilter(!negateFilter); - } else { - setNegateFilter(false); - setFilter(f); - } - }, [filter, negateFilter]); + const toggleSorting = React.useCallback((f: SortBy) => { + setSorting({ by: f, negate: sorting?.by === f ? !sorting.negate : false }); + }, [sorting]); return <> {!resource &&
- + } + render={entry => } onSelected={setResource} onHighlighted={onEntryHovered} /> @@ -73,37 +69,40 @@ export const NetworkTab: React.FunctionComponent<{ }; const NetworkHeader: React.FunctionComponent<{ - filter: Filter | undefined, - negateFilter: boolean, - toggleFilter: (filter: Filter) => void, -}> = ({ toggleFilter, filter, negateFilter }) => { - return
-
toggleFilter('status') }> + sorting: Sorting | undefined, + toggleSorting: (sortBy: SortBy) => void, +}> = ({ toggleSorting: toggleSortBy, sorting }) => { + return
+
toggleSortBy('start') }> + + +
+
toggleSortBy('status') }>  Status
-
toggleFilter('method') }> +
toggleSortBy('method') }> Method
-
toggleFilter('file') }> +
toggleSortBy('file') }> Request
-
toggleFilter('content-type') }> +
toggleSortBy('content-type') }> Content Type
-
toggleFilter('time') }> - Time +
toggleSortBy('duration') }> + Duration
-
toggleFilter('size') }> +
toggleSortBy('size') }> Size @@ -114,7 +113,8 @@ const NetworkHeader: React.FunctionComponent<{ const NetworkResource: React.FunctionComponent<{ resource: Entry, -}> = ({ resource }) => { + boundaries: Boundaries, +}> = ({ resource, boundaries }) => { const { routeStatus, resourceName, contentType } = React.useMemo(() => { const routeStatus = formatRouteStatus(resource); const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/')); @@ -126,6 +126,9 @@ const NetworkResource: React.FunctionComponent<{ }, [resource]); return
+
+
{msToString(resource._monotonicTime! - boundaries.minimum)}
+
{resource.response.status}
@@ -136,7 +139,7 @@ const NetworkResource: React.FunctionComponent<{
{resourceName}
{contentType}
-
{msToString(resource.time)}
+
{msToString(resource.time)}
{bytesToString(resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize)}
{routeStatus &&
{routeStatus}
} @@ -164,22 +167,25 @@ function formatRouteStatus(request: Entry): string { return ''; } -function sort(resources: Entry[], filter: Filter | undefined, negate: boolean) { - const c = comparator(filter); +function sort(resources: Entry[], sorting: Sorting) { + const c = comparator(sorting?.by); if (c) resources.sort(c); - if (negate) + if (sorting.negate) resources.reverse(); } -function comparator(filter: Filter | undefined) { - if (filter === 'time') +function comparator(sortBy: SortBy) { + if (sortBy === 'start') + return (a: Entry, b: Entry) => a._monotonicTime! - b._monotonicTime!; + + if (sortBy === 'duration') return (a: Entry, b: Entry) => a.time - b.time; - if (filter === 'status') + if (sortBy === 'status') return (a: Entry, b: Entry) => a.response.status - b.response.status; - if (filter === 'method') { + if (sortBy === 'method') { return (a: Entry, b: Entry) => { const valueA = a.request.method; const valueB = b.request.method; @@ -187,7 +193,7 @@ function comparator(filter: Filter | undefined) { }; } - if (filter === 'size') { + if (sortBy === 'size') { return (a: Entry, b: Entry) => { const sizeA = a.response._transferSize! > 0 ? a.response._transferSize! : a.response.bodySize; const sizeB = b.response._transferSize! > 0 ? b.response._transferSize! : b.response.bodySize; @@ -195,7 +201,7 @@ function comparator(filter: Filter | undefined) { }; } - if (filter === 'content-type') { + if (sortBy === 'content-type') { return (a: Entry, b: Entry) => { const valueA = a.response.content.mimeType; const valueB = b.response.content.mimeType; @@ -203,7 +209,7 @@ function comparator(filter: Filter | undefined) { }; } - if (filter === 'file') { + if (sortBy === 'file') { return (a: Entry, b: Entry) => { const nameA = a.request.url.substring(a.request.url.lastIndexOf('/')); const nameB = b.request.url.substring(b.request.url.lastIndexOf('/')); diff --git a/packages/trace-viewer/src/ui/timeline.tsx b/packages/trace-viewer/src/ui/timeline.tsx index 0f3df5a83d..aeea2264af 100644 --- a/packages/trace-viewer/src/ui/timeline.tsx +++ b/packages/trace-viewer/src/ui/timeline.tsx @@ -73,7 +73,7 @@ export const Timeline: React.FunctionComponent<{ rightTime: entry.endTime || boundaries.maximum, leftPosition: timeToPosition(measure.width, boundaries, entry.startTime), rightPosition: timeToPosition(measure.width, boundaries, entry.endTime || boundaries.maximum), - active: highlightedAction === entry, + active: false, }); } @@ -86,11 +86,16 @@ export const Timeline: React.FunctionComponent<{ rightTime: endTime, leftPosition: timeToPosition(measure.width, boundaries, startTime), rightPosition: timeToPosition(measure.width, boundaries, endTime), - active: highlightedEntry === resource, + active: false, }); } return bars; - }, [model, boundaries, measure, highlightedAction, highlightedEntry]); + }, [model, boundaries, measure]); + + React.useMemo(() => { + for (const bar of bars) + bar.active = (!!highlightedAction && bar.action === highlightedAction) || (!!highlightedEntry && bar.resource === highlightedEntry); + }, [bars, highlightedAction, highlightedEntry]); const onMouseDown = React.useCallback((event: React.MouseEvent) => { setPreviewPoint(undefined); diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 900f6bec5f..56bcb611ee 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -124,7 +124,7 @@ export const Workbench: React.FunctionComponent<{ const networkTab: TabbedPaneTabModel = { id: 'network', title: 'Network', - render: () => + render: () => }; const attachmentsTab: TabbedPaneTabModel = { id: 'attachments', diff --git a/packages/trace/src/har.ts b/packages/trace/src/har.ts index fa9a5e4839..fd62f7873c 100644 --- a/packages/trace/src/har.ts +++ b/packages/trace/src/har.ts @@ -41,7 +41,7 @@ export type Browser = { }; export type Page = { - startedDateTime: Date; + startedDateTime: string; id: string; title: string; pageTimings: PageTimings; @@ -56,7 +56,7 @@ export type PageTimings = { export type Entry = { pageref?: string; - startedDateTime: Date; + startedDateTime: string; time: number; request: Request; response: Response; @@ -107,7 +107,7 @@ export type Cookie = { value: string; path?: string; domain?: string; - expires?: Date; + expires?: string; httpOnly?: boolean; secure?: boolean; sameSite?: string;