From 214008317da9a088321fdf5ba7b4a82f7a653f75 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 13 Sep 2024 14:11:47 +0200 Subject: [PATCH] feat(browser): Add navigation `activationStart` timestamp to pageload span (#13658) Add the navigation performance entry `activationStart` property as a span attribute to the pageload span. The attribute is called `performance.activationStart` and it is measured in ms relative to performance.timeOrigin. --- .../suites/tracing/metrics/web-vitals/test.ts | 37 +++++++++++-------- .../tests/transactions.test.ts | 1 + .../src/metrics/browserMetrics.ts | 9 +++++ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts index 3ff09a2862c5..51a76797a23b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts @@ -59,23 +59,30 @@ sentryTest('paint web vitals values are greater than TTFB', async ({ browserName expect(fpValue).toBeGreaterThanOrEqual(ttfbValue!); }); -sentryTest('captures time origin as span attribute', async ({ getLocalTestPath, page }) => { - // Only run in chromium to ensure all vitals are present - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest( + 'captures time origin and navigation activationStart as span attributes', + async ({ getLocalTestPath, page }) => { + // Only run in chromium to ensure all vitals are present + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } - const url = await getLocalTestPath({ testDir: __dirname }); - const [eventData] = await Promise.all([getFirstSentryEnvelopeRequest(page), page.goto(url)]); + const url = await getLocalTestPath({ testDir: __dirname }); + const [eventData] = await Promise.all([getFirstSentryEnvelopeRequest(page), page.goto(url)]); - const timeOriginAttribute = eventData.contexts?.trace?.data?.['performance.timeOrigin']; - const transactionStartTimestamp = eventData.start_timestamp; + const timeOriginAttribute = eventData.contexts?.trace?.data?.['performance.timeOrigin']; + const activationStart = eventData.contexts?.trace?.data?.['performance.activationStart']; - expect(timeOriginAttribute).toBeDefined(); - expect(transactionStartTimestamp).toBeDefined(); + const transactionStartTimestamp = eventData.start_timestamp; - const delta = Math.abs(transactionStartTimestamp! - timeOriginAttribute); + expect(timeOriginAttribute).toBeDefined(); + expect(transactionStartTimestamp).toBeDefined(); - // The delta should be less than 1ms if this flakes, we should increase the threshold - expect(delta).toBeLessThanOrEqual(1); -}); + const delta = Math.abs(transactionStartTimestamp! - timeOriginAttribute); + + // The delta should be less than 1ms if this flakes, we should increase the threshold + expect(delta).toBeLessThanOrEqual(1); + + expect(activationStart).toBeGreaterThanOrEqual(0); + }, +); diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts index 861b6c420fbb..fa6b40c6a49d 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts @@ -23,6 +23,7 @@ test('Captures a pageload transaction', async ({ page }) => { 'sentry.sample_rate': 1, 'sentry.source': 'route', 'performance.timeOrigin': expect.any(Number), + 'performance.activationStart': expect.any(Number), }, op: 'pageload', span_id: expect.any(String), diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 066eba1e6839..92fe66a832ee 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -17,6 +17,7 @@ import { addTtfbInstrumentationHandler, } from './instrument'; import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils'; +import { getActivationStart } from './web-vitals/lib/getActivationStart'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; @@ -383,6 +384,14 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries // Set timeOrigin which denotes the timestamp which to base the LCP/FCP/FP/TTFB measurements on span.setAttribute('performance.timeOrigin', timeOrigin); + // In prerendering scenarios, where a page might be prefetched and pre-rendered before the user clicks the link, + // the navigation starts earlier than when the user clicks it. Web Vitals should always be based on the + // user-perceived time, so they are not reported from the actual start of the navigation, but rather from the + // time where the user actively started the navigation, for example by clicking a link. + // This is user action is called "activation" and the time between navigation and activation is stored in + // the `activationStart` attribute of the "navigation" PerformanceEntry. + span.setAttribute('performance.activationStart', getActivationStart()); + _setWebVitalAttributes(span); }