Skip to content

Commit 1ad6d58

Browse files
authored
fix(browser): Ensure standalone CLS and LCP spans have traceId of pageload span (#16864)
Fix a bug in standalone web vital spans where, if they were sent because of a navigation, they would incorrectly be added to the `navigation` span's trace instead of the initial `pageload` span's trace. This surfaced while dogfooding these spans on sentry and inspecting them in the EAP traceview. To fix this, I added a lifecycle hook in #16863 that triggers right before we start preparing the navigation span and hence, right before we recycle the propagation context to the new traceId. This patch now makes use of the new hook for CLS and LCP spans and it also makes the integration tests stricter to check for the correct trace ids.
1 parent 1864b22 commit 1ad6d58

File tree

4 files changed

+24
-12
lines changed
  • dev-packages/browser-integration-tests/suites/tracing/metrics
    • web-vitals-cls-standalone-spans
    • web-vitals-lcp-standalone-spans
  • packages/browser-utils/src/metrics

4 files changed

+24
-12
lines changed

dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,10 +348,10 @@ sentryTest(
348348
sentryTest('sends CLS of the initial page when soft-navigating to a new page', async ({ getLocalTestUrl, page }) => {
349349
const url = await getLocalTestUrl({ testDir: __dirname });
350350

351-
const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
351+
const pageloadEventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
352352

353-
expect(eventData.type).toBe('transaction');
354-
expect(eventData.contexts?.trace?.op).toBe('pageload');
353+
expect(pageloadEventData.type).toBe('transaction');
354+
expect(pageloadEventData.contexts?.trace?.op).toBe('pageload');
355355

356356
const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
357357
page,
@@ -364,12 +364,16 @@ sentryTest('sends CLS of the initial page when soft-navigating to a new page', a
364364

365365
await page.goto(`${url}#soft-navigation`);
366366

367+
const pageloadTraceId = pageloadEventData.contexts?.trace?.trace_id;
368+
expect(pageloadTraceId).toMatch(/[a-f0-9]{32}/);
369+
367370
const spanEnvelope = (await spanEnvelopePromise)[0];
368371
const spanEnvelopeItem = spanEnvelope[1][0][1];
369372
// Flakey value dependent on timings -> we check for a range
370373
expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.05);
371374
expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.15);
372-
expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toMatch(/[a-f0-9]{16}/);
375+
expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadEventData.contexts?.trace?.span_id);
376+
expect(spanEnvelopeItem.trace_id).toEqual(pageloadTraceId);
373377
});
374378

375379
sentryTest("doesn't send further CLS after the first navigation", async ({ getLocalTestUrl, page }) => {

dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { expect } from '@playwright/test';
33
import type { Event as SentryEvent, EventEnvelope, SpanEnvelope } from '@sentry/core';
44
import { sentryTest } from '../../../../utils/fixtures';
55
import {
6+
envelopeRequestParser,
67
getFirstSentryEnvelopeRequest,
78
getMultipleSentryEnvelopeRequests,
89
properFullEnvelopeRequestParser,
910
shouldSkipTracingTest,
11+
waitForTransactionRequest,
1012
} from '../../../../utils/helpers';
1113

1214
sentryTest.beforeEach(async ({ browserName, page }) => {
@@ -31,6 +33,8 @@ sentryTest('captures LCP vital as a standalone span', async ({ getLocalTestUrl,
3133
properFullEnvelopeRequestParser,
3234
);
3335

36+
const pageloadEnvelopePromise = waitForTransactionRequest(page, e => e.contexts?.trace?.op === 'pageload');
37+
3438
page.route('**', route => route.continue());
3539
page.route('**/my/image.png', async (route: Route) => {
3640
return route.fulfill({
@@ -47,10 +51,14 @@ sentryTest('captures LCP vital as a standalone span', async ({ getLocalTestUrl,
4751
await hidePage(page);
4852

4953
const spanEnvelope = (await spanEnvelopePromise)[0];
54+
const pageloadTransactionEvent = envelopeRequestParser(await pageloadEnvelopePromise);
5055

5156
const spanEnvelopeHeaders = spanEnvelope[0];
5257
const spanEnvelopeItem = spanEnvelope[1][0][1];
5358

59+
const pageloadTraceId = pageloadTransactionEvent.contexts?.trace?.trace_id;
60+
expect(pageloadTraceId).toMatch(/[a-f0-9]{32}/);
61+
5462
expect(spanEnvelopeItem).toEqual({
5563
data: {
5664
'sentry.exclusive_time': 0,
@@ -80,7 +88,7 @@ sentryTest('captures LCP vital as a standalone span', async ({ getLocalTestUrl,
8088
segment_id: expect.stringMatching(/[a-f0-9]{16}/),
8189
start_timestamp: expect.any(Number),
8290
timestamp: spanEnvelopeItem.start_timestamp, // LCP is a point-in-time metric
83-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
91+
trace_id: pageloadTraceId,
8492
});
8593

8694
// LCP value should be greater than 0
@@ -95,7 +103,6 @@ sentryTest('captures LCP vital as a standalone span', async ({ getLocalTestUrl,
95103
sampled: 'true',
96104
trace_id: spanEnvelopeItem.trace_id,
97105
sample_rand: expect.any(String),
98-
// no transaction, because span source is URL
99106
},
100107
});
101108
});
@@ -152,10 +159,10 @@ sentryTest('sends LCP of the initial page when soft-navigating to a new page', a
152159

153160
const url = await getLocalTestUrl({ testDir: __dirname });
154161

155-
const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
162+
const pageloadEventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
156163

157-
expect(eventData.type).toBe('transaction');
158-
expect(eventData.contexts?.trace?.op).toBe('pageload');
164+
expect(pageloadEventData.type).toBe('transaction');
165+
expect(pageloadEventData.contexts?.trace?.op).toBe('pageload');
159166

160167
const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
161168
page,
@@ -173,7 +180,8 @@ sentryTest('sends LCP of the initial page when soft-navigating to a new page', a
173180
const spanEnvelopeItem = spanEnvelope[1][0][1];
174181

175182
expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0);
176-
expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toMatch(/[a-f0-9]{16}/);
183+
expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadEventData.contexts?.trace?.span_id);
184+
expect(spanEnvelopeItem.trace_id).toBe(pageloadEventData.contexts?.trace?.trace_id);
177185
});
178186

179187
sentryTest("doesn't send further LCP after the first navigation", async ({ getLocalTestUrl, page }) => {

packages/browser-utils/src/metrics/cls.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function trackClsAsStandaloneSpan(): void {
7272
return;
7373
}
7474

75-
const unsubscribeStartNavigation = client.on('startNavigationSpan', (_, options) => {
75+
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
7676
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
7777
if (!options?.isRedirect) {
7878
_collectClsOnce();

packages/browser-utils/src/metrics/lcp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function trackLcpAsStandaloneSpan(): void {
7272
return;
7373
}
7474

75-
const unsubscribeStartNavigation = client.on('startNavigationSpan', (_, options) => {
75+
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
7676
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
7777
if (!options?.isRedirect) {
7878
_collectLcpOnce();

0 commit comments

Comments
 (0)