diff --git a/.size-limit.js b/.size-limit.js
index 24772d8380f5..5eb520d68684 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -243,7 +243,7 @@ module.exports = [
import: createImport('init'),
ignore: ['$app/stores'],
gzip: true,
- limit: '42 KB',
+ limit: '42.5 KB',
},
// Node-Core SDK (ESM)
{
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/subject.js
new file mode 100644
index 000000000000..bfaa54db4d01
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/subject.js
@@ -0,0 +1,16 @@
+const errorBtn = document.getElementById('errorBtn');
+errorBtn.addEventListener('click', () => {
+ throw new Error(`Sentry Test Error ${Math.random()}`);
+});
+
+const fetchBtn = document.getElementById('fetchBtn');
+fetchBtn.addEventListener('click', async () => {
+ await fetch('http://sentry-test-site.example');
+});
+
+const xhrBtn = document.getElementById('xhrBtn');
+xhrBtn.addEventListener('click', () => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', 'http://sentry-test-site.example');
+ xhr.send();
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/template.html
new file mode 100644
index 000000000000..a3c17f442605
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/template.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/test.ts
new file mode 100644
index 000000000000..76e015dcb07a
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/test.ts
@@ -0,0 +1,85 @@
+import { expect } from '@playwright/test';
+import { sentryTest } from '../../../../utils/fixtures';
+import type { EventAndTraceHeader } from '../../../../utils/helpers';
+import {
+ eventAndTraceHeaderRequestParser,
+ getFirstSentryEnvelopeRequest,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+const META_TAG_TRACE_ID = '12345678901234567890123456789012';
+const META_TAG_PARENT_SPAN_ID = '1234567890123456';
+const META_TAG_BAGGAGE =
+ 'sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=0.2,sentry-sampled=true,sentry-transaction=my-transaction,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.42';
+
+sentryTest(
+ 'create a new trace for a navigation after the server timing headers',
+ async ({ getLocalTestUrl, page, enableConsole }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ enableConsole();
+
+ const url = await getLocalTestUrl({
+ testDir: __dirname,
+ responseHeaders: {
+ 'Server-Timing': `sentry-trace;desc=${META_TAG_TRACE_ID}-${META_TAG_PARENT_SPAN_ID}-1, baggage;desc="${META_TAG_BAGGAGE}"`,
+ },
+ });
+
+ const [pageloadEvent, pageloadTraceHeader] = await getFirstSentryEnvelopeRequest(
+ page,
+ url,
+ eventAndTraceHeaderRequestParser,
+ );
+ const [navigationEvent, navigationTraceHeader] = await getFirstSentryEnvelopeRequest(
+ page,
+ `${url}#foo`,
+ eventAndTraceHeaderRequestParser,
+ );
+
+ const pageloadTraceContext = pageloadEvent.contexts?.trace;
+ const navigationTraceContext = navigationEvent.contexts?.trace;
+
+ expect(pageloadEvent.type).toEqual('transaction');
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[\da-f]{16}$/),
+ });
+
+ expect(pageloadTraceHeader).toEqual({
+ environment: 'prod',
+ release: '1.0.0',
+ sample_rate: '0.2',
+ sampled: 'true',
+ transaction: 'my-transaction',
+ public_key: 'public',
+ trace_id: META_TAG_TRACE_ID,
+ sample_rand: '0.42',
+ });
+
+ expect(navigationEvent.type).toEqual('transaction');
+ expect(navigationTraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[\da-f]{32}$/),
+ span_id: expect.stringMatching(/^[\da-f]{16}$/),
+ });
+ // navigation span is head of trace, so there's no parent span:
+ expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
+
+ expect(navigationTraceHeader).toEqual({
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: navigationTraceContext?.trace_id,
+ sample_rand: expect.any(String),
+ });
+
+ expect(pageloadTraceContext?.trace_id).not.toEqual(navigationTraceContext?.trace_id);
+ expect(pageloadTraceHeader?.sample_rand).not.toEqual(navigationTraceHeader?.sample_rand);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts
index 4269f91c2c07..c66d8793e610 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts
@@ -64,7 +64,7 @@ sentryTest(
span_id: expect.stringMatching(/^[\da-f]{16}$/),
});
// navigation span is head of trace, so there's no parent span:
- expect(navigationTraceContext?.trace_id).not.toHaveProperty('parent_span_id');
+ expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
expect(navigationTraceHeader).toEqual({
environment: 'production',
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/init.js
new file mode 100644
index 000000000000..94222159056c
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/init.js
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ // in browser TwP means not setting tracesSampleRate but adding browserTracingIntegration,
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracePropagationTargets: ['http://sentry-test-site.example'],
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/template.html
new file mode 100644
index 000000000000..a29ad2056a45
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/test.ts
new file mode 100644
index 000000000000..3d0262b717d9
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/test.ts
@@ -0,0 +1,51 @@
+import { expect } from '@playwright/test';
+import { sentryTest } from '../../../../utils/fixtures';
+import type { EventAndTraceHeader } from '../../../../utils/helpers';
+import {
+ eventAndTraceHeaderRequestParser,
+ getFirstSentryEnvelopeRequest,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+const META_TAG_TRACE_ID = '12345678901234567890123456789012';
+const META_TAG_PARENT_SPAN_ID = '1234567890123456';
+const META_TAG_BAGGAGE =
+ 'sentry-trace_id=12345678901234567890123456789012,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.42';
+
+sentryTest('error on initial page has traceId from server timing headers', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({
+ testDir: __dirname,
+ responseHeaders: {
+ 'Server-Timing': `sentry-trace;desc=${META_TAG_TRACE_ID}-${META_TAG_PARENT_SPAN_ID}, baggage;desc="${META_TAG_BAGGAGE}"`,
+ },
+ });
+ await page.goto(url);
+
+ const errorEventPromise = getFirstSentryEnvelopeRequest(
+ page,
+ undefined,
+ eventAndTraceHeaderRequestParser,
+ );
+
+ await page.locator('#errorBtn').click();
+ const [errorEvent, errorTraceHeader] = await errorEventPromise;
+
+ expect(errorEvent.type).toEqual(undefined);
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[\da-f]{16}$/),
+ });
+
+ expect(errorTraceHeader).toEqual({
+ environment: 'prod',
+ public_key: 'public',
+ release: '1.0.0',
+ trace_id: META_TAG_TRACE_ID,
+ sample_rand: '0.42',
+ });
+});
diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts
index 2e3eebe86845..c71acf106258 100644
--- a/packages/browser/src/tracing/browserTracingIntegration.ts
+++ b/packages/browser/src/tracing/browserTracingIntegration.ts
@@ -594,8 +594,9 @@ export const browserTracingIntegration = ((options: Partial entry.name === name);
+ return entry?.description;
+}
+
/** Start listener for interaction transactions */
function registerInteractionListener(
client: Client,
diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts
index e3f1060655c2..991fcc1393a4 100644
--- a/packages/browser/test/tracing/browserTracingIntegration.test.ts
+++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts
@@ -25,6 +25,7 @@ import { BrowserClient } from '../../src/client';
import { WINDOW } from '../../src/helpers';
import {
browserTracingIntegration,
+ getServerTiming,
startBrowserTracingNavigationSpan,
startBrowserTracingPageLoadSpan,
} from '../../src/tracing/browserTracingIntegration';
@@ -72,6 +73,7 @@ describe('browserTracingIntegration', () => {
afterEach(() => {
getActiveSpan()?.end();
vi.useRealTimers();
+ vi.restoreAllMocks();
performance.clearMarks();
});
@@ -1029,6 +1031,212 @@ describe('browserTracingIntegration', () => {
});
});
+ describe('getServerTiming', () => {
+ it('retrieves server timing description when available', () => {
+ // Mock the performance API
+ const mockServerTiming = [
+ { name: 'sentry-trace', duration: 0, description: '12312012123120121231201212312012-1121201211212012-1' },
+ { name: 'baggage', duration: 0, description: 'sentry-release=2.1.14,sentry-sample_rand=0.456' },
+ ];
+
+ const mockNavigationEntry = {
+ serverTiming: mockServerTiming,
+ };
+
+ vi.spyOn(WINDOW.performance, 'getEntriesByType').mockReturnValue([mockNavigationEntry as any]);
+
+ const sentryTrace = getServerTiming('sentry-trace');
+ const baggage = getServerTiming('baggage');
+
+ expect(sentryTrace).toBe('12312012123120121231201212312012-1121201211212012-1');
+ expect(baggage).toBe('sentry-release=2.1.14,sentry-sample_rand=0.456');
+ });
+
+ it('returns undefined when server timing entry is not found', () => {
+ const mockServerTiming = [{ name: 'other-timing', duration: 0, description: 'some-value' }];
+
+ const mockNavigationEntry = {
+ serverTiming: mockServerTiming,
+ };
+
+ vi.spyOn(WINDOW.performance, 'getEntriesByType').mockReturnValue([mockNavigationEntry as any]);
+
+ const result = getServerTiming('sentry-trace');
+
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined when performance API is not available', () => {
+ const originalPerformance = WINDOW.performance;
+ // @ts-expect-error - intentionally setting to undefined
+ WINDOW.performance = undefined;
+
+ const result = getServerTiming('sentry-trace');
+
+ expect(result).toBeUndefined();
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore its read only
+ WINDOW.performance = originalPerformance;
+ });
+
+ it('returns undefined when serverTiming is not available', () => {
+ const mockNavigationEntry = {
+ serverTiming: undefined,
+ };
+
+ vi.spyOn(WINDOW.performance, 'getEntriesByType').mockReturnValue([mockNavigationEntry as any]);
+
+ const result = getServerTiming('sentry-trace');
+
+ expect(result).toBeUndefined();
+ });
+ });
+
+ describe('using Server-Timing headers', () => {
+ it('uses Server-Timing headers for pageload span', () => {
+ // Mock the performance API with Server-Timing data
+ const mockServerTiming = [
+ { name: 'sentry-trace', duration: 0, description: '12312012123120121231201212312012-1121201211212012-0' },
+ { name: 'baggage', duration: 0, description: 'sentry-release=2.1.14,foo=bar,sentry-sample_rand=0.123' },
+ ];
+
+ const mockNavigationEntry = {
+ serverTiming: mockServerTiming,
+ };
+
+ vi.spyOn(WINDOW.performance, 'getEntriesByType').mockReturnValue([mockNavigationEntry as any]);
+
+ const client = new BrowserClient(
+ getDefaultBrowserClientOptions({
+ tracesSampleRate: 1,
+ integrations: [browserTracingIntegration()],
+ }),
+ );
+ setCurrentClient(client);
+
+ // pageload transactions are created as part of the browserTracingIntegration's initialization
+ client.init();
+
+ const idleSpan = getActiveSpan()!;
+ expect(idleSpan).toBeDefined();
+
+ const dynamicSamplingContext = getDynamicSamplingContextFromSpan(idleSpan);
+ const propagationContext = getCurrentScope().getPropagationContext();
+
+ // Span is correct
+ expect(spanToJSON(idleSpan).op).toBe('pageload');
+ expect(spanToJSON(idleSpan).trace_id).toEqual('12312012123120121231201212312012');
+ expect(spanToJSON(idleSpan).parent_span_id).toEqual('1121201211212012');
+ expect(spanIsSampled(idleSpan)).toBe(false);
+
+ expect(dynamicSamplingContext).toBeDefined();
+ expect(dynamicSamplingContext).toStrictEqual({ release: '2.1.14', sample_rand: '0.123' });
+
+ // Propagation context keeps the Server-Timing trace data for later events on the same route to add them to the trace
+ expect(propagationContext.traceId).toEqual('12312012123120121231201212312012');
+ expect(propagationContext.parentSpanId).toEqual('1121201211212012');
+ expect(propagationContext.sampleRand).toBe(0.123);
+ });
+
+ it('meta tags take precedence over Server-Timing headers', () => {
+ // Set up both meta tags and Server-Timing headers
+ document.head.innerHTML =
+ '' +
+ '';
+
+ const mockServerTiming = [
+ { name: 'sentry-trace', duration: 0, description: '12312012123120121231201212312012-1121201211212012-0' },
+ { name: 'baggage', duration: 0, description: 'sentry-release=2.1.14,sentry-sample_rand=0.123' },
+ ];
+
+ const mockNavigationEntry = {
+ serverTiming: mockServerTiming,
+ };
+
+ vi.spyOn(WINDOW.performance, 'getEntriesByType').mockReturnValue([mockNavigationEntry as any]);
+
+ const client = new BrowserClient(
+ getDefaultBrowserClientOptions({
+ tracesSampleRate: 1,
+ integrations: [browserTracingIntegration()],
+ }),
+ );
+ setCurrentClient(client);
+
+ client.init();
+
+ const idleSpan = getActiveSpan()!;
+ expect(idleSpan).toBeDefined();
+
+ const dynamicSamplingContext = getDynamicSamplingContextFromSpan(idleSpan);
+ const propagationContext = getCurrentScope().getPropagationContext();
+
+ // Span should use meta tag data, not Server-Timing data
+ expect(spanToJSON(idleSpan).trace_id).toEqual('11111111111111111111111111111111');
+ expect(spanToJSON(idleSpan).parent_span_id).toEqual('2222222222222222');
+ expect(spanIsSampled(idleSpan)).toBe(true);
+
+ expect(dynamicSamplingContext).toStrictEqual({ release: '3.0.0', sample_rand: '0.999' });
+
+ expect(propagationContext.traceId).toEqual('11111111111111111111111111111111');
+ expect(propagationContext.parentSpanId).toEqual('2222222222222222');
+ expect(propagationContext.sampleRand).toBe(0.999);
+ });
+
+ it('uses passed in tracing data over Server-Timing headers', () => {
+ const mockServerTiming = [
+ { name: 'sentry-trace', duration: 0, description: '12312012123120121231201212312012-1121201211212012-0' },
+ { name: 'baggage', duration: 0, description: 'sentry-release=2.1.14,sentry-sample_rand=0.123' },
+ ];
+
+ const mockNavigationEntry = {
+ serverTiming: mockServerTiming,
+ };
+
+ vi.spyOn(WINDOW.performance, 'getEntriesByType').mockReturnValue([mockNavigationEntry as any]);
+
+ const client = new BrowserClient(
+ getDefaultBrowserClientOptions({
+ tracesSampleRate: 1,
+ integrations: [browserTracingIntegration({ instrumentPageLoad: false })],
+ }),
+ );
+ setCurrentClient(client);
+
+ client.init();
+
+ // manually create a pageload span with tracing data
+ startBrowserTracingPageLoadSpan(
+ client,
+ {
+ name: 'test span',
+ },
+ {
+ sentryTrace: '99999999999999999999999999999999-8888888888888888-1',
+ baggage: 'sentry-release=4.0.0,sentry-sample_rand=0.777',
+ },
+ );
+
+ const idleSpan = getActiveSpan()!;
+ expect(idleSpan).toBeDefined();
+
+ const dynamicSamplingContext = getDynamicSamplingContextFromSpan(idleSpan);
+ const propagationContext = getCurrentScope().getPropagationContext();
+
+ // Span should use passed-in data, not Server-Timing data
+ expect(spanToJSON(idleSpan).trace_id).toEqual('99999999999999999999999999999999');
+ expect(spanToJSON(idleSpan).parent_span_id).toEqual('8888888888888888');
+ expect(spanIsSampled(idleSpan)).toBe(true);
+
+ expect(dynamicSamplingContext).toStrictEqual({ release: '4.0.0', sample_rand: '0.777' });
+
+ expect(propagationContext.traceId).toEqual('99999999999999999999999999999999');
+ expect(propagationContext.parentSpanId).toEqual('8888888888888888');
+ expect(propagationContext.sampleRand).toBe(0.777);
+ });
+ });
+
describe('idleTimeout', () => {
it('is created by default', () => {
vi.useFakeTimers();