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-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts
index 870558ada39d..7a6a4e0d7b81 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,
@@ -604,6 +606,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 +617,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 ? 0x1 : 0x0,
+ },
+ 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 cd63678de16f..6a28d897b234 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;
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;
}
/**