Skip to content

fix(browser): Ensure standalone CLS and LCP spans have traceId of pageload span #16864

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -348,10 +348,10 @@ sentryTest(
sentryTest('sends CLS of the initial page when soft-navigating to a new page', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
const pageloadEventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);

expect(eventData.type).toBe('transaction');
expect(eventData.contexts?.trace?.op).toBe('pageload');
expect(pageloadEventData.type).toBe('transaction');
expect(pageloadEventData.contexts?.trace?.op).toBe('pageload');

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

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

const pageloadTraceId = pageloadEventData.contexts?.trace?.trace_id;
expect(pageloadTraceId).toMatch(/[a-f0-9]{32}/);

const spanEnvelope = (await spanEnvelopePromise)[0];
const spanEnvelopeItem = spanEnvelope[1][0][1];
// Flakey value dependent on timings -> we check for a range
expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.05);
expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.15);
expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toMatch(/[a-f0-9]{16}/);
expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadEventData.contexts?.trace?.span_id);
expect(spanEnvelopeItem.trace_id).toEqual(pageloadTraceId);
});

sentryTest("doesn't send further CLS after the first navigation", async ({ getLocalTestUrl, page }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { expect } from '@playwright/test';
import type { Event as SentryEvent, EventEnvelope, SpanEnvelope } from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
import {
envelopeRequestParser,
getFirstSentryEnvelopeRequest,
getMultipleSentryEnvelopeRequests,
properFullEnvelopeRequestParser,
shouldSkipTracingTest,
waitForTransactionRequest,
} from '../../../../utils/helpers';

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

const pageloadEnvelopePromise = waitForTransactionRequest(page, e => e.contexts?.trace?.op === 'pageload');

page.route('**', route => route.continue());
page.route('**/my/image.png', async (route: Route) => {
return route.fulfill({
Expand All @@ -47,10 +51,14 @@ sentryTest('captures LCP vital as a standalone span', async ({ getLocalTestUrl,
await hidePage(page);

const spanEnvelope = (await spanEnvelopePromise)[0];
const pageloadTransactionEvent = envelopeRequestParser(await pageloadEnvelopePromise);

const spanEnvelopeHeaders = spanEnvelope[0];
const spanEnvelopeItem = spanEnvelope[1][0][1];

const pageloadTraceId = pageloadTransactionEvent.contexts?.trace?.trace_id;
expect(pageloadTraceId).toMatch(/[a-f0-9]{32}/);

expect(spanEnvelopeItem).toEqual({
data: {
'sentry.exclusive_time': 0,
Expand Down Expand Up @@ -80,7 +88,7 @@ sentryTest('captures LCP vital as a standalone span', async ({ getLocalTestUrl,
segment_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
timestamp: spanEnvelopeItem.start_timestamp, // LCP is a point-in-time metric
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
trace_id: pageloadTraceId,
});

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

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

const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
const pageloadEventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);

expect(eventData.type).toBe('transaction');
expect(eventData.contexts?.trace?.op).toBe('pageload');
expect(pageloadEventData.type).toBe('transaction');
expect(pageloadEventData.contexts?.trace?.op).toBe('pageload');

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

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

sentryTest("doesn't send further LCP after the first navigation", async ({ getLocalTestUrl, page }) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/metrics/cls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function trackClsAsStandaloneSpan(): void {
return;
}

const unsubscribeStartNavigation = client.on('startNavigationSpan', (_, options) => {
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
if (!options?.isRedirect) {
_collectClsOnce();
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/metrics/lcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function trackLcpAsStandaloneSpan(): void {
return;
}

const unsubscribeStartNavigation = client.on('startNavigationSpan', (_, options) => {
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
if (!options?.isRedirect) {
_collectLcpOnce();
Expand Down
Loading