From c8e90453031c53192b590fa7c5e827808bf6e544 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 4 Sep 2025 09:25:03 +0200 Subject: [PATCH 1/3] feat(browser): Add experimental `parentlessRootSpans` `browserTracingIntegration` option --- .../interactions/test.ts | 2 +- .../meta-parentless/init.js | 10 ++ .../meta-parentless/template.html | 11 +++ .../meta-parentless/test.ts | 93 +++++++++++++++++++ .../twp-errors-meta-parentless/init.js | 12 +++ .../twp-errors-meta-parentless/subject.js | 2 + .../twp-errors-meta-parentless/template.html | 11 +++ .../twp-errors-meta-parentless/test.ts | 35 +++++++ .../interactions-meta-parentless/init.js | 18 ++++ .../interactions-meta-parentless/subject.js | 18 ++++ .../template.html | 19 ++++ .../interactions-meta-parentless/test.ts | 45 +++++++++ .../trace-lifetime/interactions-meta/init.js | 17 ++++ .../interactions-meta/subject.js | 18 ++++ .../interactions-meta/template.html | 19 ++++ .../trace-lifetime/interactions-meta/test.ts | 47 ++++++++++ .../src/tracing/browserTracingIntegration.ts | 13 ++- 17 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts index 8ce215311cf9..382b65c25958 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -7,7 +7,7 @@ import { shouldSkipTracingTest, } from '../../../../utils/helpers'; -sentryTest('should capture interaction transaction. @firefox', async ({ browserName, getLocalTestUrl, page }) => { +sentryTest('captures interaction span @firefox', async ({ browserName, getLocalTestUrl, page }) => { const supportedBrowsers = ['chromium', 'firefox']; if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/init.js new file mode 100644 index 000000000000..644cca6118a1 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration({ _experiments: { parentlessRootSpans: true } })], + tracesSampleRate: 1, + environment: 'staging', +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/template.html new file mode 100644 index 000000000000..7f7b0b159fee --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/template.html @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/test.ts new file mode 100644 index 000000000000..c8591844080c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta-parentless/test.ts @@ -0,0 +1,93 @@ +import { expect } from '@playwright/test'; +import type { Event, EventEnvelopeHeaders } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeHeaderRequestParser, + getFirstSentryEnvelopeRequest, + shouldSkipTracingTest, +} from '../../../../utils/helpers'; + +sentryTest( + 'creates a pageload span based on `sentry-trace` without parent span id if parentlessRootSpans is true', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.contexts?.trace).toMatchObject({ + op: 'pageload', + trace_id: '12312012123120121231201212312012', + }); + expect(eventData.contexts?.trace?.parent_span_id).toBeUndefined(); + + expect(eventData.spans?.length).toBeGreaterThan(0); + }, +); + +sentryTest( + 'picks up `baggage` tag, propagate the content in transaction and not add own data', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser); + + expect(envHeader.trace).toBeDefined(); + expect(envHeader.trace).toEqual({ + release: '2.1.12', + sample_rate: '0.3232', + trace_id: '123', + public_key: 'public', + sample_rand: '0.42', + }); + }, +); + +sentryTest("creates a navigation that's not influenced by `sentry-trace` ", async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequest = await getFirstSentryEnvelopeRequest(page, url); + const navigationRequest = await getFirstSentryEnvelopeRequest(page, `${url}#foo`); + + expect(pageloadRequest.contexts?.trace).toMatchObject({ + op: 'pageload', + trace_id: '12312012123120121231201212312012', + }); + expect(pageloadRequest.contexts?.trace?.parent_span_id).toBeUndefined(); + + expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); + expect(navigationRequest.contexts?.trace?.trace_id).toBeDefined(); + expect(navigationRequest.contexts?.trace?.trace_id).not.toBe(pageloadRequest.contexts?.trace?.trace_id); + + const pageloadSpans = pageloadRequest.spans; + const navigationSpans = navigationRequest.spans; + + const pageloadSpanId = pageloadRequest.contexts?.trace?.span_id; + const navigationSpanId = navigationRequest.contexts?.trace?.span_id; + + expect(pageloadSpanId).toBeDefined(); + expect(navigationSpanId).toBeDefined(); + + pageloadSpans?.forEach(span => + expect(span).toMatchObject({ + parent_span_id: pageloadSpanId, + }), + ); + + navigationSpans?.forEach(span => + expect(span).toMatchObject({ + parent_span_id: navigationSpanId, + }), + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/init.js new file mode 100644 index 000000000000..949d853d6b5f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: integrations => { + integrations.push(Sentry.browserTracingIntegration({ _experiments: { parentlessRootSpans: true } })); + return integrations.filter(i => i.name !== 'BrowserSession'); + }, + tracesSampleRate: 0, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/subject.js new file mode 100644 index 000000000000..b7d62f8cfb95 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/subject.js @@ -0,0 +1,2 @@ +Sentry.captureException(new Error('test error')); +Sentry.captureException(new Error('test error 2')); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/template.html new file mode 100644 index 000000000000..22d155bf8648 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/template.html @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/test.ts new file mode 100644 index 000000000000..8a3b7b5703b5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta-parentless/test.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/browser'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('errors in TwP mode have same trace ID & span IDs', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const META_TRACE_ID = '12312012123120121231201212312012'; + const META_PARENT_SPAN_ID = '1121201211212012'; + + const url = await getLocalTestUrl({ testDir: __dirname }); + const [event1, event2] = await getMultipleSentryEnvelopeRequests(page, 2, { url }); + + // Ensure these are the actual errors we care about + expect(event1.exception?.values?.[0].value).toContain('test error'); + expect(event2.exception?.values?.[0].value).toContain('test error'); + + const contexts1 = event1.contexts; + const { trace_id: traceId1, span_id: spanId1 } = contexts1?.trace || {}; + expect(traceId1).toEqual(META_TRACE_ID); + + // Span ID is a virtual span in TwP mode, not the propagated one + expect(spanId1).not.toEqual(META_PARENT_SPAN_ID); + expect(spanId1).toMatch(/^[a-f0-9]{16}$/); + + const contexts2 = event2.contexts; + const { trace_id: traceId2, span_id: spanId2 } = contexts2?.trace || {}; + expect(traceId2).toEqual(META_TRACE_ID); + expect(spanId2).toMatch(/^[a-f0-9]{16}$/); + + expect(spanId2).toEqual(spanId1); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/init.js new file mode 100644 index 000000000000..ae815b5f103d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 1000, + enableLongTask: false, + _experiments: { + enableInteractions: true, + parentlessRootSpans: true, + }, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/subject.js new file mode 100644 index 000000000000..0d4772ba535d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/subject.js @@ -0,0 +1,18 @@ +const blockUI = e => { + const startTime = Date.now(); + + function getElapsed() { + const time = Date.now(); + return time - startTime; + } + + while (getElapsed() < 70) { + // + } + + e.target.classList.add('clicked'); +}; + +document.querySelector('[data-test-id=interaction-button]').addEventListener('click', blockUI); +document.querySelector('[data-test-id=annotated-button]').addEventListener('click', blockUI); +document.querySelector('[data-test-id=styled-button]').addEventListener('click', blockUI); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/template.html new file mode 100644 index 000000000000..ea63086f12f5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/template.html @@ -0,0 +1,19 @@ + + + + + + + + +
Rendered Before Long Task
+ + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/test.ts new file mode 100644 index 000000000000..f0345308e148 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta-parentless/test.ts @@ -0,0 +1,45 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +const META_TAG_TRACE_ID = '12312012123120121231201212312012'; + +sentryTest( + 'interaction spans continue trace from tag after pageload', + async ({ browserName, getLocalTestUrl, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequestPromise = waitForTransactionRequest(page, e => e.contexts?.trace?.op === 'pageload'); + await page.goto(url); + const pageloadTxnEvent = envelopeRequestParser(await pageloadRequestPromise); + + const interactionRequestPromise = waitForTransactionRequest(page, e => e.contexts?.trace?.op === 'ui.action.click'); + + await page.locator('[data-test-id=interaction-button]').click(); + await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); + + const interactionTxnEvent = envelopeRequestParser(await interactionRequestPromise); + + const pageloadTxnTraceContext = pageloadTxnEvent.contexts?.trace; + const interactionTxnTraceContext = interactionTxnEvent.contexts?.trace; + + expect(pageloadTxnTraceContext).toMatchObject({ + trace_id: META_TAG_TRACE_ID, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + expect(pageloadTxnTraceContext?.parent_span_id).toBeUndefined(); + + expect(interactionTxnTraceContext).toMatchObject({ + trace_id: META_TAG_TRACE_ID, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + expect(interactionTxnTraceContext?.parent_span_id).toBeUndefined(); + + expect(interactionTxnTraceContext?.trace_id).toBe(pageloadTxnTraceContext?.trace_id); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/init.js new file mode 100644 index 000000000000..846538e7f3f0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 1000, + enableLongTask: false, + _experiments: { + enableInteractions: true, + }, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/subject.js new file mode 100644 index 000000000000..0d4772ba535d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/subject.js @@ -0,0 +1,18 @@ +const blockUI = e => { + const startTime = Date.now(); + + function getElapsed() { + const time = Date.now(); + return time - startTime; + } + + while (getElapsed() < 70) { + // + } + + e.target.classList.add('clicked'); +}; + +document.querySelector('[data-test-id=interaction-button]').addEventListener('click', blockUI); +document.querySelector('[data-test-id=annotated-button]').addEventListener('click', blockUI); +document.querySelector('[data-test-id=styled-button]').addEventListener('click', blockUI); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/template.html new file mode 100644 index 000000000000..ea63086f12f5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/template.html @@ -0,0 +1,19 @@ + + + + + + + + +
Rendered Before Long Task
+ + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/test.ts new file mode 100644 index 000000000000..9e5344908b17 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/interactions-meta/test.ts @@ -0,0 +1,47 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +const META_TAG_TRACE_ID = '12312012123120121231201212312012'; +const META_TAG_PARENT_SPAN_ID = '1121201211212012'; + +sentryTest( + 'interaction spans continue trace from tag after pageload', + async ({ browserName, getLocalTestUrl, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequestPromise = waitForTransactionRequest(page, e => e.contexts?.trace?.op === 'pageload'); + await page.goto(url); + const pageloadTxnEvent = envelopeRequestParser(await pageloadRequestPromise); + + const interactionRequestPromise = waitForTransactionRequest(page, e => e.contexts?.trace?.op === 'ui.action.click'); + + await page.locator('[data-test-id=interaction-button]').click(); + await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); + + const interactionTxnEvent = envelopeRequestParser(await interactionRequestPromise); + + const pageloadTxnTraceContext = pageloadTxnEvent.contexts?.trace; + const interactionTxnTraceContext = interactionTxnEvent.contexts?.trace; + + expect(pageloadTxnTraceContext).toMatchObject({ + trace_id: META_TAG_TRACE_ID, + parent_span_id: META_TAG_PARENT_SPAN_ID, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + + expect(interactionTxnTraceContext).toMatchObject({ + trace_id: META_TAG_TRACE_ID, + parent_span_id: META_TAG_PARENT_SPAN_ID, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + + expect(interactionTxnTraceContext?.trace_id).toBe(pageloadTxnTraceContext?.trace_id); + expect(interactionTxnTraceContext?.parent_span_id).toBe(pageloadTxnTraceContext?.parent_span_id); + }, +); diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index cd63678de16f..69611e274a7a 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -257,6 +257,13 @@ export interface BrowserTracingOptions { enableInteractions: boolean; enableStandaloneClsSpans: boolean; enableStandaloneLcpSpans: boolean; + + /** + * If `true`, root spans started in the browser (pageload, navigation, ui.action.*, manual spans) + * will not have a parent span id, even if there was a parent SSR span propagated to the browser + * (via `` tags). + */ + parentlessRootSpans: boolean; }>; /** @@ -325,7 +332,7 @@ export const browserTracingIntegration = ((_options: Partial Date: Thu, 4 Sep 2025 09:48:49 +0200 Subject: [PATCH 2/3] span link --- .../src/metrics/browserMetrics.ts | 18 ++++++++++++++++++ .../src/tracing/browserTracingIntegration.ts | 4 +++- packages/core/src/types-hoist/link.ts | 2 +- packages/core/src/types-hoist/tracing.ts | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 870558ada39d..ab64f265a390 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -4,10 +4,12 @@ import { browserPerformanceTimeOrigin, getActiveSpan, getComponentName, + getCurrentScope, htmlTreeAsString, isPrimitive, parseUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, setMeasurement, spanToJSON, stringMatchesSomePattern, @@ -32,6 +34,7 @@ import { import { getActivationStart } from './web-vitals/lib/getActivationStart'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; +import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '@sentry/core/build/types/utils/spanUtils'; interface NavigatorNetworkInformation { readonly connection?: NetworkInformation; @@ -604,6 +607,7 @@ function _addRequest(span: Span, entry: PerformanceNavigationTiming, timeOrigin: const responseEndTimestamp = timeOrigin + msToSec(entry.responseEnd as number); const responseStartTimestamp = timeOrigin + msToSec(entry.responseStart as number); if (entry.responseEnd) { + const propagationContext = getCurrentScope().getPropagationContext(); // It is possible that we are collecting these metrics when the page hasn't finished loading yet, for example when the HTML slowly streams in. // In this case, ie. when the document request hasn't finished yet, `entry.responseEnd` will be 0. // In order not to produce faulty spans, where the end timestamp is before the start timestamp, we will only collect @@ -614,6 +618,20 @@ function _addRequest(span: Span, entry: PerformanceNavigationTiming, timeOrigin: attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, + ...(propagationContext.ssrSpanId && { + links: [ + { + context: { + traceId: propagationContext.traceId, + spanId: propagationContext.ssrSpanId, + traceFlags: propagationContext.sampled ? TRACE_FLAG_SAMPLED : TRACE_FLAG_NONE, + }, + attributes: { + [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'ssr_span', + }, + }, + ], + }), }); startAndEndSpan(span, responseStartTimestamp, responseEndTimestamp, { diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 69611e274a7a..6a28d897b234 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -547,7 +547,9 @@ export const browserTracingIntegration = ((_options: Partial; export interface SpanLink { diff --git a/packages/core/src/types-hoist/tracing.ts b/packages/core/src/types-hoist/tracing.ts index e1dcfef96c6a..0a577a56ccee 100644 --- a/packages/core/src/types-hoist/tracing.ts +++ b/packages/core/src/types-hoist/tracing.ts @@ -50,6 +50,12 @@ export interface PropagationContext { * The current SDK should not modify this value! */ dsc?: Partial; + + /** + * The id of a server-side span that describe the SSR request handling lifecycle (most likely the root `http.server` span). + * To be used on the client for additional annotation if present. + */ + ssrSpanId?: string; } /** From 94ee89d3191525dd9ee68518a4cd7137f4fa3444 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 4 Sep 2025 10:01:47 +0200 Subject: [PATCH 3/3] fix trace flags import --- packages/browser-utils/src/metrics/browserMetrics.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index ab64f265a390..7a6a4e0d7b81 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -34,7 +34,6 @@ import { import { getActivationStart } from './web-vitals/lib/getActivationStart'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; -import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '@sentry/core/build/types/utils/spanUtils'; interface NavigatorNetworkInformation { readonly connection?: NetworkInformation; @@ -624,7 +623,7 @@ function _addRequest(span: Span, entry: PerformanceNavigationTiming, timeOrigin: context: { traceId: propagationContext.traceId, spanId: propagationContext.ssrSpanId, - traceFlags: propagationContext.sampled ? TRACE_FLAG_SAMPLED : TRACE_FLAG_NONE, + traceFlags: propagationContext.sampled ? 0x1 : 0x0, }, attributes: { [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'ssr_span',