From 76c8652e324535a21a807725e960b9c5055bd295 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Wed, 15 Nov 2023 09:23:11 +0000 Subject: [PATCH] feat: performance event display fiddling (#18629) * only show wrapping data for fetch and xnr * show prettier timing * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- ...components-networkrequesttiming--basic.png | Bin 0 -> 9145 bytes .../components/ItemPerformanceEvent.tsx | 124 ++++---- .../Timing/NetworkRequestTiming.stories.tsx | 57 ++++ .../Timing/NetworkRequestTiming.tsx | 280 ++++++++++++++++++ frontend/src/styles/utilities.scss | 20 ++ 5 files changed, 430 insertions(+), 51 deletions(-) create mode 100644 frontend/__snapshots__/components-networkrequesttiming--basic.png create mode 100644 frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx create mode 100644 frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx diff --git a/frontend/__snapshots__/components-networkrequesttiming--basic.png b/frontend/__snapshots__/components-networkrequesttiming--basic.png new file mode 100644 index 0000000000000000000000000000000000000000..abff737987dfec5506630342d64c819d049f14bc GIT binary patch literal 9145 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 literal 0 HcmV?d00001 diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx index 50bb076efd35b..cb1493077b010 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx @@ -9,6 +9,7 @@ import { Fragment, useState } from 'react' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { FEATURE_FLAGS } from 'lib/constants' +import { NetworkRequestTiming } from 'scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming' const friendlyHttpStatus = { '0': 'Request not sent', @@ -179,6 +180,10 @@ export function ItemPerformanceEvent({ return acc } + if (key.includes('time') || key.includes('end') || key.includes('start')) { + return acc + } + return { ...acc, [key]: typeof value === 'number' ? Math.round(value) : value, @@ -334,58 +339,75 @@ export function ItemPerformanceEvent({

)} - - - setActiveTab(newKey)} - tabs={[ - { - key: 'timings', - label: 'timings', - content: , - }, - { - key: 'headers', - label: 'Headers', - content: ( - - ), - }, - item.entry_type !== 'navigation' && { - key: 'payload', - label: 'Payload', - content: ( - - ), - }, - item.entry_type !== 'navigation' && item.response_body - ? { - key: 'response_body', - label: 'Response', - content: ( - - ), - } - : false, - ]} - /> - - - - + {['fetch', 'xmlhttprequest'].includes(item.initiator_type || '') ? ( + <> + + setActiveTab(newKey)} + tabs={[ + { + key: 'timings', + label: 'timings', + content: ( + <> + + + + + ), + }, + { + key: 'headers', + label: 'Headers', + content: ( + + ), + }, + item.entry_type !== 'navigation' && { + key: 'payload', + label: 'Payload', + content: ( + + ), + }, + item.entry_type !== 'navigation' && item.response_body + ? { + key: 'response_body', + label: 'Response', + content: ( + + ), + } + : false, + ]} + /> + + + + + + + + ) : ( + <> + + + + + )} )} diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx new file mode 100644 index 0000000000000..a02e9bf3dce03 --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx @@ -0,0 +1,57 @@ +import { mswDecorator } from '~/mocks/browser' +import { Meta } from '@storybook/react' +import { PerformanceEvent } from '~/types' +import { NetworkRequestTiming } from 'scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming' + +const meta: Meta = { + title: 'Components/NetworkRequestTiming', + component: NetworkRequestTiming, + decorators: [ + mswDecorator({ + get: {}, + }), + ], +} +export default meta + +export function Basic(): JSX.Element { + return ( + + ) +} diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx new file mode 100644 index 0000000000000..d9b97dac0f26f --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx @@ -0,0 +1,280 @@ +import { PerformanceEvent } from '~/types' +import { getSeriesColor } from 'lib/colors' +import { humanFriendlyMilliseconds } from 'lib/utils' +import { Tooltip } from 'lib/lemon-ui/Tooltip' + +function colorForEntry(entryType: string | undefined): string { + switch (entryType) { + case 'domComplete': + return getSeriesColor(1) + case 'domInteractive': + return getSeriesColor(2) + case 'pageLoaded': + return getSeriesColor(3) + case 'first-contentful-paint': + return getSeriesColor(4) + case 'css': + return getSeriesColor(6) + case 'xmlhttprequest': + return getSeriesColor(7) + case 'fetch': + return getSeriesColor(8) + case 'other': + return getSeriesColor(9) + case 'script': + return getSeriesColor(10) + case 'link': + return getSeriesColor(11) + case 'first-paint': + return getSeriesColor(11) + default: + return getSeriesColor(13) + } +} + +export interface EventPerformanceMeasure { + start: number + end: number + color: string + reducedHeight?: boolean +} + +const perfSections = [ + 'redirect', + 'app cache', + 'dns lookup', + 'connection time', + 'tls time', + 'waiting for first byte (TTFB)', + 'receiving response', + 'document processing', +] as const + +const perfDescriptions: Record<(typeof perfSections)[number], string> = { + redirect: + 'The time it took to fetch any previous resources that redirected to this one. If either redirect_start or redirect_end timestamp is 0, there were no redirects, or one of the redirects wasn’t from the same origin as this resource.', + 'app cache': 'The time taken to check the application cache or fetch the resource from the application cache.', + 'dns lookup': 'The time taken to complete any DNS lookup for the resource.', + 'connection time': 'The time taken to establish a connection to the server to retrieve the resource.', + 'tls time': 'The time taken for the SSL/TLS handshake.', + 'waiting for first byte (TTFB)': 'The time taken waiting for the server to start returning a response.', + 'receiving response': 'The time taken to receive the response from the server.', + 'document processing': + 'The time taken to process the document after the response from the server has been received.', +} + +function colorForSection(section: (typeof perfSections)[number]): string { + switch (section) { + case 'redirect': + return getSeriesColor(1) + case 'app cache': + return getSeriesColor(2) + case 'dns lookup': + return getSeriesColor(3) + case 'connection time': + return getSeriesColor(4) + case 'tls time': + return getSeriesColor(6) + case 'waiting for first byte (TTFB)': + return getSeriesColor(7) + case 'receiving response': + return getSeriesColor(8) + case 'document processing': + return getSeriesColor(9) + default: + return getSeriesColor(10) + } +} + +/** + * There are defined sections to performance measurement. We may have data for some or all of them + * + * 1) Redirect + * - from startTime which would also be redirectStart + * - until redirect_end + * + * 2) App Cache + * - from fetch_start + * - until domain_lookup_start + * + * 3) DNS + * - from domain_lookup_start + * - until domain_lookup_end + * + * 4) TCP + * - from connect_start + * - until connect_end + * + * this contains any time to negotiate SSL/TLS + * - from secure_connection_start + * - until connect_end + * + * 5) Request + * - from request_start + * - until response_start + * + * 6) Response + * - from response_start + * - until response_end + * + * 7) Document Processing + * - from response_end + * - until load_event_end + * + * see https://nicj.net/resourcetiming-in-practice/ + * + * @param perfEntry + * @param maxTime + */ +function calculatePerformanceParts(perfEntry: PerformanceEvent): Record { + const performanceParts: Record = {} + + if (perfEntry.redirect_start && perfEntry.redirect_end) { + performanceParts['redirect'] = { + start: perfEntry.redirect_start, + end: perfEntry.redirect_end, + color: colorForEntry(perfEntry.initiator_type), + } + } + + if (perfEntry.fetch_start && perfEntry.domain_lookup_start) { + performanceParts['app cache'] = { + start: perfEntry.fetch_start, + end: perfEntry.domain_lookup_start, + color: colorForEntry(perfEntry.initiator_type), + } + } + + if (perfEntry.domain_lookup_end && perfEntry.domain_lookup_start) { + performanceParts['dns lookup'] = { + start: perfEntry.domain_lookup_start, + end: perfEntry.domain_lookup_end, + color: colorForEntry(perfEntry.initiator_type), + } + } + + if (perfEntry.connect_end && perfEntry.connect_start) { + performanceParts['connection time'] = { + start: perfEntry.connect_start, + end: perfEntry.connect_end, + color: colorForEntry(perfEntry.initiator_type), + } + + if (perfEntry.secure_connection_start) { + performanceParts['tls time'] = { + start: perfEntry.secure_connection_start, + end: perfEntry.connect_end, + color: colorForEntry(perfEntry.initiator_type), + reducedHeight: true, + } + } + } + + if (perfEntry.response_start && perfEntry.request_start) { + performanceParts['waiting for first byte (TTFB)'] = { + start: perfEntry.request_start, + end: perfEntry.response_start, + color: colorForEntry(perfEntry.initiator_type), + } + } + + if (perfEntry.response_start && perfEntry.response_end) { + performanceParts['receiving response'] = { + start: perfEntry.response_start, + end: perfEntry.response_end, + color: colorForEntry(perfEntry.initiator_type), + } + } + + if (perfEntry.response_end && perfEntry.load_event_end) { + performanceParts['document processing'] = { + start: perfEntry.response_end, + end: perfEntry.load_event_end, + color: colorForEntry(perfEntry.initiator_type), + } + } + + return performanceParts +} + +function percentagesWithinEventRange({ + partStart, + partEnd, + rangeEnd, + rangeStart, +}: { + partStart: number + partEnd: number + rangeStart: number + rangeEnd: number +}): { startPercentage: string; widthPercentage: string } { + const totalDuration = rangeEnd - rangeStart + const partStartRelativeToTimeline = partStart - rangeStart + const partDuration = partEnd - partStart + + const partPercentage = (partDuration / totalDuration) * 100 + const partStartPercentage = (partStartRelativeToTimeline / totalDuration) * 100 + return { startPercentage: `${partStartPercentage}%`, widthPercentage: `${partPercentage}%` } +} + +export const NetworkRequestTiming = ({ + performanceEvent, +}: { + performanceEvent: PerformanceEvent +}): JSX.Element | null => { + const rangeStart = performanceEvent.start_time + const rangeEnd = performanceEvent.response_end + if (typeof rangeStart === 'number' && typeof rangeEnd === 'number') { + const performanceParts = calculatePerformanceParts(performanceEvent) + return ( +
+ {perfSections.map((section) => { + const matchedSection = performanceParts[section] + const start = matchedSection?.start + const end = matchedSection?.end + const partDuration = end - start + let formattedDuration: string | undefined + let startPercentage = null + let widthPercentage = null + + if (isNaN(partDuration) || partDuration === 0) { + formattedDuration = '' + } else { + formattedDuration = humanFriendlyMilliseconds(partDuration) + const percentages = percentagesWithinEventRange({ + rangeStart, + rangeEnd, + partStart: start, + partEnd: end, + }) + startPercentage = percentages.startPercentage + widthPercentage = percentages.widthPercentage + } + + return ( + <> +
+
+ {section} +
+
+
+
+
{formattedDuration || ''}
+
+ + ) + })} +
+ ) + } + return null +} diff --git a/frontend/src/styles/utilities.scss b/frontend/src/styles/utilities.scss index 8e27162b8c7e0..0f26bf65d4b11 100644 --- a/frontend/src/styles/utilities.scss +++ b/frontend/src/styles/utilities.scss @@ -503,6 +503,26 @@ $decorations: underline, overline, line-through, no-underline; .#{$variant}#{$char}-4\/5 { #{$variant}#{$kind}: 80%; } + + .#{$variant}#{$char}-1\/6 { + #{$variant}#{$kind}: 16.66667%; + } + + .#{$variant}#{$char}-2\/6 { + #{$variant}#{$kind}: 33.33333%; + } + + .#{$variant}#{$char}-3\/6 { + #{$variant}#{$kind}: 50%; + } + + .#{$variant}#{$char}-4\/6 { + #{$variant}#{$kind}: 66.66667%; + } + + .#{$variant}#{$char}-5\/6 { + #{$variant}#{$kind}: 83.33333%; + } } }