diff --git a/.size-limit.js b/.size-limit.js
index 6e005fd7c3e7..3b91cb51a3c5 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -208,7 +208,7 @@ module.exports = [
'tls',
],
gzip: true,
- limit: '150 KB',
+ limit: '160 KB',
},
];
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c7657346696..1501c66636f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,35 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 8.0.0-beta.2
+
+### Important Changes
+
+- **feat(browser): Update `propagationContext` on `spanEnd` to keep trace consistent**
+
+To ensure consistency throughout a route's duration, we update the scope's propagation context when the initial page
+load or navigation span ends. This keeps span-specific attributes like the sampled decision and dynamic sampling context
+on the scope, even after the transaction has ended.
+
+- **fix(browser): Don't assume window.document is available (#11602)**
+
+We won't assume `window.dodument` is available in the browser SDKs anymore. This should prevent errors in environments
+where `window.document` is not available (such as web workers).
+
+### Other changes
+
+- feat(core): Add `server.address` to browser `http.client` spans (#11634)
+- feat(opentelemetry): Update OTEL packages & relax some version ranges (#11580)
+- feat(deps): bump @opentelemetry/instrumentation-hapi from 0.34.0 to 0.36.0 (#11496)
+- feat(deps): bump @opentelemetry/instrumentation-koa from 0.37.0 to 0.39.0 (#11495)
+- feat(deps): bump @opentelemetry/instrumentation-pg from 0.38.0 to 0.40.0 (#11494)
+- feat(nextjs): Skip OTEL root spans emitted by Next.js (#11623)
+- feat(node): Collect Local Variables via a worker (#11586)
+- fix(nextjs): Escape Next.js' OpenTelemetry instrumentation (#11625)
+- fix(feedback): Fix timeout on feedback submission (#11619)
+- fix(node): Allow use of `NodeClient` without calling `init` (#11585)
+- fix(node): Ensure DSC is correctly set in envelope headers (#11628)
+
## 8.0.0-beta.1
This is the first beta release of Sentry JavaScript SDK v8. With this release, there are no more planned breaking
diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json
index 0256e573518b..9352ca625497 100644
--- a/dev-packages/browser-integration-tests/package.json
+++ b/dev-packages/browser-integration-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/browser-integration-tests",
- "version": "8.0.0-beta.1",
+ "version": "8.0.0-beta.2",
"main": "index.js",
"license": "MIT",
"engines": {
@@ -42,7 +42,7 @@
"@babel/preset-typescript": "^7.16.7",
"@playwright/test": "^1.40.1",
"@sentry-internal/rrweb": "2.11.0",
- "@sentry/browser": "8.0.0-beta.1",
+ "@sentry/browser": "8.0.0-beta.2",
"axios": "1.6.7",
"babel-loader": "^8.2.2",
"html-webpack-plugin": "^5.5.0",
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/init.js
new file mode 100644
index 000000000000..83076460599f
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/init.js
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/subject.js
new file mode 100644
index 000000000000..b0d4abc78c65
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/subject.js
@@ -0,0 +1,3 @@
+fetch('/test-req/0').then(
+ fetch('/test-req/1', { headers: { 'X-Test-Header': 'existing-header' } }).then(fetch('/test-req/2')),
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts
new file mode 100644
index 000000000000..d4a6065a177b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts
@@ -0,0 +1,79 @@
+import { expect } from '@playwright/test';
+
+import { TEST_HOST, sentryTest } from '../../../../utils/fixtures';
+import {
+ envelopeRequestParser,
+ shouldSkipTracingTest,
+ waitForTransactionRequestOnUrl,
+} from '../../../../utils/helpers';
+
+sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+ const req = await waitForTransactionRequestOnUrl(page, url);
+ const tracingEvent = envelopeRequestParser(req);
+
+ const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client');
+
+ expect(requestSpans).toHaveLength(3);
+
+ requestSpans?.forEach((span, index) =>
+ expect(span).toMatchObject({
+ description: `GET /test-req/${index}`,
+ parent_span_id: tracingEvent.contexts?.trace?.span_id,
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: tracingEvent.contexts?.trace?.trace_id,
+ data: {
+ 'http.method': 'GET',
+ 'http.url': `${TEST_HOST}/test-req/${index}`,
+ url: `/test-req/${index}`,
+ 'server.address': 'sentry-test.io',
+ type: 'fetch',
+ },
+ }),
+ );
+});
+
+sentryTest('should attach `sentry-trace` header to fetch requests', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`${TEST_HOST}/test-req/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ const request1 = requests[0];
+ const requestHeaders1 = request1.headers();
+ expect(requestHeaders1).toMatchObject({
+ 'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
+ baggage: expect.any(String),
+ });
+
+ const request2 = requests[1];
+ const requestHeaders2 = request2.headers();
+ expect(requestHeaders2).toMatchObject({
+ 'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
+ baggage: expect.any(String),
+ 'x-test-header': 'existing-header',
+ });
+
+ const request3 = requests[2];
+ const requestHeaders3 = request3.headers();
+ expect(requestHeaders3).toMatchObject({
+ 'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
+ baggage: expect.any(String),
+ });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts
index 00cf0baafc6a..de6b1521d686 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts
@@ -36,6 +36,13 @@ sentryTest('should create spans for fetch requests', async ({ getLocalTestPath,
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: tracingEvent.contexts?.trace?.trace_id,
+ data: {
+ 'http.method': 'GET',
+ 'http.url': `http://example.com/${index}`,
+ url: `http://example.com/${index}`,
+ 'server.address': 'example.com',
+ type: 'fetch',
+ },
}),
);
});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/init.js
new file mode 100644
index 000000000000..83076460599f
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/init.js
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/subject.js
new file mode 100644
index 000000000000..5fc9f91ab568
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/subject.js
@@ -0,0 +1,12 @@
+const xhr_1 = new XMLHttpRequest();
+xhr_1.open('GET', '/test-req/0');
+xhr_1.send();
+
+const xhr_2 = new XMLHttpRequest();
+xhr_2.open('GET', '/test-req/1');
+xhr_2.setRequestHeader('X-Test-Header', 'existing-header');
+xhr_2.send();
+
+const xhr_3 = new XMLHttpRequest();
+xhr_3.open('GET', '/test-req/2');
+xhr_3.send();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts
new file mode 100644
index 000000000000..f3dca9359b8b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts
@@ -0,0 +1,79 @@
+import { expect } from '@playwright/test';
+
+import { TEST_HOST, sentryTest } from '../../../../utils/fixtures';
+import {
+ envelopeRequestParser,
+ shouldSkipTracingTest,
+ waitForTransactionRequestOnUrl,
+} from '../../../../utils/helpers';
+
+sentryTest('should create spans for xhr requests', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+ const req = await waitForTransactionRequestOnUrl(page, url);
+ const tracingEvent = envelopeRequestParser(req);
+
+ const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client');
+
+ expect(requestSpans).toHaveLength(3);
+
+ requestSpans?.forEach((span, index) =>
+ expect(span).toMatchObject({
+ description: `GET /test-req/${index}`,
+ parent_span_id: tracingEvent.contexts?.trace?.span_id,
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: tracingEvent.contexts?.trace?.trace_id,
+ data: {
+ 'http.method': 'GET',
+ 'http.url': `${TEST_HOST}/test-req/${index}`,
+ url: `/test-req/${index}`,
+ 'server.address': 'sentry-test.io',
+ type: 'xhr',
+ },
+ }),
+ );
+});
+
+sentryTest('should attach `sentry-trace` header to xhr requests', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`${TEST_HOST}/test-req/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ const request1 = requests[0];
+ const requestHeaders1 = request1.headers();
+ expect(requestHeaders1).toMatchObject({
+ 'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
+ baggage: expect.any(String),
+ });
+
+ const request2 = requests[1];
+ const requestHeaders2 = request2.headers();
+ expect(requestHeaders2).toMatchObject({
+ 'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
+ baggage: expect.any(String),
+ 'x-test-header': 'existing-header',
+ });
+
+ const request3 = requests[2];
+ const requestHeaders3 = request3.headers();
+ expect(requestHeaders3).toMatchObject({
+ 'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
+ baggage: expect.any(String),
+ });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts
index 13646a34826e..5dbfd3edf4cb 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts
@@ -24,6 +24,13 @@ sentryTest('should create spans for XHR requests', async ({ getLocalTestPath, pa
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: eventData.contexts?.trace?.trace_id,
+ data: {
+ 'http.method': 'GET',
+ 'http.url': `http://example.com/${index}`,
+ url: `http://example.com/${index}`,
+ 'server.address': 'example.com',
+ type: 'xhr',
+ },
}),
);
});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/README.md b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/README.md
new file mode 100644
index 000000000000..3413aac3bcbf
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/README.md
@@ -0,0 +1,10 @@
+Tests in this suite are meant to test the lifetime of a trace in the browser SDK and how different events sent are
+connected to a trace. This suite distinguishes the following cases:
+
+1. `pageload` - Traces started on the initial pageload as head of trace
+2. `pageload-meta` - Traces started on the initial pageload as a continuation of the trace on the server (via ``
+ tags)
+3. `navigation` - Traces started during navigations on a page
+4. `tracing-without-performance` - Traces originating from an app configured for "Tracing without Performance".
+
+Tests scenarios should be fairly similar for all three cases but it's important we test all of them.
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/init.js
index 83076460599f..7cd076a052e5 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/init.js
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/init.js
@@ -5,5 +5,6 @@ window.Sentry = Sentry;
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
+ tracePropagationTargets: ['http://example.com'],
tracesSampleRate: 1,
});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts
index 559fba5a8e06..f24ca0507c66 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts
@@ -1,9 +1,13 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';
import { sentryTest } from '../../../../utils/fixtures';
-import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+import {
+ getFirstSentryEnvelopeRequest,
+ getMultipleSentryEnvelopeRequests,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
-sentryTest('should create a new trace on each navigation', async ({ getLocalTestPath, page }) => {
+sentryTest('creates a new trace on each navigation', async ({ getLocalTestPath, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}
@@ -14,15 +18,24 @@ sentryTest('should create a new trace on each navigation', async ({ getLocalTest
const navigationEvent1 = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
const navigationEvent2 = await getFirstSentryEnvelopeRequest(page, `${url}#bar`);
- expect(navigationEvent1.contexts?.trace?.op).toBe('navigation');
- expect(navigationEvent2.contexts?.trace?.op).toBe('navigation');
+ const navigation1TraceContext = navigationEvent1.contexts?.trace;
+ const navigation2TraceContext = navigationEvent2.contexts?.trace;
- const navigation1TraceId = navigationEvent1.contexts?.trace?.trace_id;
- const navigation2TraceId = navigationEvent2.contexts?.trace?.trace_id;
+ expect(navigation1TraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(navigation1TraceContext).not.toHaveProperty('parent_span_id');
- expect(navigation1TraceId).toMatch(/^[0-9a-f]{32}$/);
- expect(navigation2TraceId).toMatch(/^[0-9a-f]{32}$/);
- expect(navigation1TraceId).not.toEqual(navigation2TraceId);
+ expect(navigation2TraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(navigation2TraceContext).not.toHaveProperty('parent_span_id');
+
+ expect(navigation1TraceContext?.trace_id).not.toEqual(navigation2TraceContext?.trace_id);
});
sentryTest('error after navigation has navigation traceId', async ({ getLocalTestPath, page }) => {
@@ -32,20 +45,205 @@ sentryTest('error after navigation has navigation traceId', async ({ getLocalTes
const url = await getLocalTestPath({ testDir: __dirname });
+ // ensure pageload transaction is finished
+ await getFirstSentryEnvelopeRequest(page, url);
+
+ const navigationEvent = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
+ const navigationTraceContext = navigationEvent.contexts?.trace;
+
+ expect(navigationTraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
+
+ const errorEventPromise = getFirstSentryEnvelopeRequest(page);
+ await page.locator('#errorBtn').click();
+ const errorEvent = await errorEventPromise;
+
+ const errorTraceContext = errorEvent.contexts?.trace;
+ expect(errorTraceContext).toEqual({
+ trace_id: navigationTraceContext?.trace_id,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+});
+
+sentryTest('error during navigation has new navigation traceId', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
// ensure navigation transaction is finished
await getFirstSentryEnvelopeRequest(page, url);
- const navigationEvent1 = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
- expect(navigationEvent1.contexts?.trace?.op).toBe('navigation');
+ const envelopeRequestsPromise = getMultipleSentryEnvelopeRequests(page, 2);
+ await page.goto(`${url}#foo`);
+ await page.locator('#errorBtn').click();
+ const events = await envelopeRequestsPromise;
- const navigationTraceId = navigationEvent1.contexts?.trace?.trace_id;
- expect(navigationTraceId).toMatch(/^[0-9a-f]{32}$/);
+ const navigationEvent = events.find(event => event.type === 'transaction');
+ const errorEvent = events.find(event => !event.type);
- const [, errorEvent] = await Promise.all([
- page.locator('#errorBtn').click(),
- getFirstSentryEnvelopeRequest(page),
- ]);
+ const navigationTraceContext = navigationEvent?.contexts?.trace;
+ expect(navigationTraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
- const errorTraceId = errorEvent.contexts?.trace?.trace_id;
- expect(errorTraceId).toBe(navigationTraceId);
+ const errorTraceContext = errorEvent?.contexts?.trace;
+ expect(errorTraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: errorTraceContext?.trace_id,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
});
+
+sentryTest(
+ 'outgoing fetch request after navigation has navigation traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ // ensure navigation transaction is finished
+ await getFirstSentryEnvelopeRequest(page, url);
+
+ const navigationEvent = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
+
+ const navigationTraceContext = navigationEvent.contexts?.trace;
+ expect(navigationTraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
+
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.locator('#fetchBtn').click();
+ const request = await requestPromise;
+ const headers = request.headers();
+
+ // sampling decision and DSC are continued from navigation span, even after it ended
+ const navigationTraceId = navigationTraceContext?.trace_id;
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toEqual(
+ `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true`,
+ );
+ },
+);
+
+sentryTest(
+ 'outgoing fetch request during navigation has navigation traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ // ensure navigation transaction is finished
+ await getFirstSentryEnvelopeRequest(page, url);
+
+ const navigationEventPromise = getFirstSentryEnvelopeRequest(page);
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.goto(`${url}#foo`);
+ await page.locator('#fetchBtn').click();
+ const [navigationEvent, request] = await Promise.all([navigationEventPromise, requestPromise]);
+
+ const navigationTraceContext = navigationEvent.contexts?.trace;
+ expect(navigationTraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
+
+ const headers = request.headers();
+
+ // sampling decision is propagated from active span sampling decision
+ const navigationTraceId = navigationTraceContext?.trace_id;
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toEqual(
+ `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true`,
+ );
+ },
+);
+
+sentryTest(
+ 'outgoing XHR request after navigation has navigation traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ // ensure navigation transaction is finished
+ await getFirstSentryEnvelopeRequest(page, url);
+
+ const navigationEvent = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
+
+ const navigationTraceContext = navigationEvent.contexts?.trace;
+ expect(navigationTraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
+
+ const xhrPromise = page.waitForRequest('http://example.com/*');
+ await page.locator('#xhrBtn').click();
+ const request = await xhrPromise;
+ const headers = request.headers();
+
+ // sampling decision and DSC are continued from navigation span, even after it ended
+ const navigationTraceId = navigationTraceContext?.trace_id;
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toEqual(
+ `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true`,
+ );
+ },
+);
+
+sentryTest(
+ 'outgoing XHR request during navigation has navigation traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ // ensure navigation transaction is finished
+ await getFirstSentryEnvelopeRequest(page, url);
+
+ const navigationEventPromise = getFirstSentryEnvelopeRequest(page);
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.goto(`${url}#foo`);
+ await page.locator('#xhrBtn').click();
+ const [navigationEvent, request] = await Promise.all([navigationEventPromise, requestPromise]);
+
+ const navigationTraceContext = navigationEvent.contexts?.trace;
+ expect(navigationTraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
+ const headers = request.headers();
+
+ // sampling decision is propagated from active span sampling decision
+ const navigationTraceId = navigationTraceContext?.trace_id;
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toEqual(
+ `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true`,
+ );
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/template.html
new file mode 100644
index 000000000000..0dee204aef16
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 000000000000..75e1d4f1c3b6
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts
@@ -0,0 +1,217 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+import { sentryTest } from '../../../../utils/fixtures';
+import {
+ getFirstSentryEnvelopeRequest,
+ getMultipleSentryEnvelopeRequests,
+ 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';
+
+sentryTest(
+ 'create a new trace for a navigation after the tag pageload trace',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url);
+ const navigationEvent = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
+
+ const pageloadTraceContext = pageloadEvent.contexts?.trace;
+ const navigationTraceContext = navigationEvent.contexts?.trace;
+
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(navigationTraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ // navigation span is head of trace, so there's no parent span:
+ expect(navigationTraceContext?.trace_id).not.toHaveProperty('parent_span_id');
+
+ expect(pageloadTraceContext?.trace_id).not.toEqual(navigationTraceContext?.trace_id);
+ },
+);
+
+sentryTest('error after tag pageload has pageload traceId', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url);
+ expect(pageloadEvent.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+
+ const errorEventPromise = getFirstSentryEnvelopeRequest(page);
+ await page.locator('#errorBtn').click();
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.contexts?.trace).toMatchObject({
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+});
+
+sentryTest('error during tag pageload has pageload traceId', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const envelopeRequestsPromise = getMultipleSentryEnvelopeRequests(page, 2);
+ await page.goto(url);
+ await page.locator('#errorBtn').click();
+ const events = await envelopeRequestsPromise;
+
+ const pageloadEvent = events.find(event => event.type === 'transaction');
+ const errorEvent = events.find(event => !event.type);
+
+ expect(pageloadEvent?.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+
+ expect(errorEvent?.contexts?.trace).toMatchObject({
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+});
+
+sentryTest(
+ 'outgoing fetch request after tag pageload has pageload traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url);
+ expect(pageloadEvent?.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.locator('#fetchBtn').click();
+ const request = await requestPromise;
+ const headers = request.headers();
+
+ // sampling decision is propagated from meta tag's sentry-trace sampled flag
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${META_TAG_TRACE_ID}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toBe(META_TAG_BAGGAGE);
+ },
+);
+
+sentryTest(
+ 'outgoing fetch request during tag pageload has pageload traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadEventPromise = getFirstSentryEnvelopeRequest(page);
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.goto(url);
+ await page.locator('#fetchBtn').click();
+ const [pageloadEvent, request] = await Promise.all([pageloadEventPromise, requestPromise]);
+
+ expect(pageloadEvent?.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+
+ const headers = request.headers();
+
+ // sampling decision is propagated from meta tag's sentry-trace sampled flag
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${META_TAG_TRACE_ID}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toBe(META_TAG_BAGGAGE);
+ },
+);
+
+sentryTest(
+ 'outgoing XHR request after tag pageload has pageload traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url);
+ expect(pageloadEvent?.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.locator('#xhrBtn').click();
+ const request = await requestPromise;
+ const headers = request.headers();
+
+ // sampling decision is propagated from meta tag's sentry-trace sampled flag
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${META_TAG_TRACE_ID}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toBe(META_TAG_BAGGAGE);
+ },
+);
+
+sentryTest(
+ 'outgoing XHR request during tag pageload has pageload traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadEventPromise = getFirstSentryEnvelopeRequest(page);
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.goto(url);
+ await page.locator('#xhrBtn').click();
+ const [pageloadEvent, request] = await Promise.all([pageloadEventPromise, requestPromise]);
+
+ expect(pageloadEvent?.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ trace_id: META_TAG_TRACE_ID,
+ parent_span_id: META_TAG_PARENT_SPAN_ID,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+
+ const headers = request.headers();
+
+ // sampling decision is propagated from meta tag's sentry-trace sampled flag
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${META_TAG_TRACE_ID}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toBe(META_TAG_BAGGAGE);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts
index 16659f013dd0..dc87dea9760b 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts
@@ -1,7 +1,11 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';
import { sentryTest } from '../../../../utils/fixtures';
-import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+import {
+ getFirstSentryEnvelopeRequest,
+ getMultipleSentryEnvelopeRequests,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
sentryTest(
'should create a new trace for a navigation after the initial pageload',
@@ -13,17 +17,26 @@ sentryTest(
const url = await getLocalTestPath({ testDir: __dirname });
const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url);
- const navigationEvent1 = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
+ const navigationEvent = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
- expect(pageloadEvent.contexts?.trace?.op).toBe('pageload');
- expect(navigationEvent1.contexts?.trace?.op).toBe('navigation');
+ const pageloadTraceContext = pageloadEvent.contexts?.trace;
+ const navigationTraceContext = navigationEvent.contexts?.trace;
- const pageloadTraceId = pageloadEvent.contexts?.trace?.trace_id;
- const navigation1TraceId = navigationEvent1.contexts?.trace?.trace_id;
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(pageloadTraceContext).not.toHaveProperty('parent_span_id');
- expect(pageloadTraceId).toMatch(/^[0-9a-f]{32}$/);
- expect(navigation1TraceId).toMatch(/^[0-9a-f]{32}$/);
- expect(pageloadTraceId).not.toEqual(navigation1TraceId);
+ expect(navigationTraceContext).toMatchObject({
+ op: 'navigation',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
+
+ expect(pageloadTraceContext?.span_id).not.toEqual(navigationTraceContext?.span_id);
},
);
@@ -35,16 +48,188 @@ sentryTest('error after pageload has pageload traceId', async ({ getLocalTestPat
const url = await getLocalTestPath({ testDir: __dirname });
const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url);
- expect(pageloadEvent.contexts?.trace?.op).toBe('pageload');
+ const pageloadTraceContext = pageloadEvent.contexts?.trace;
+
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(pageloadTraceContext).not.toHaveProperty('parent_span_id');
+
+ const errorEventPromise = getFirstSentryEnvelopeRequest(page);
+ await page.locator('#errorBtn').click();
+ const errorEvent = await errorEventPromise;
+
+ const errorTraceContext = errorEvent.contexts?.trace;
+
+ expect(errorTraceContext).toEqual({
+ trace_id: pageloadTraceContext?.trace_id,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+});
+
+sentryTest('error during pageload has pageload traceId', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
- const pageloadTraceId = pageloadEvent.contexts?.trace?.trace_id;
- expect(pageloadTraceId).toMatch(/^[0-9a-f]{32}$/);
+ const envelopeRequestsPromise = getMultipleSentryEnvelopeRequests(page, 2);
+ await page.goto(url);
+ await page.locator('#errorBtn').click();
+ const events = await envelopeRequestsPromise;
- const [, errorEvent] = await Promise.all([
- page.locator('#errorBtn').click(),
- getFirstSentryEnvelopeRequest(page),
- ]);
+ const pageloadEvent = events.find(event => event.type === 'transaction');
+ const errorEvent = events.find(event => !event.type);
- const errorTraceId = errorEvent.contexts?.trace?.trace_id;
- expect(errorTraceId).toBe(pageloadTraceId);
+ const pageloadTraceContext = pageloadEvent?.contexts?.trace;
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(pageloadTraceContext).not.toHaveProperty('parent_span_id');
+
+ const errorTraceContext = errorEvent?.contexts?.trace;
+ expect(errorTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: pageloadTraceContext?.trace_id,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
});
+
+sentryTest(
+ 'outgoing fetch request after pageload has pageload traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url);
+ const pageloadTraceContext = pageloadEvent.contexts?.trace;
+
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(pageloadTraceContext).not.toHaveProperty('parent_span_id');
+
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.locator('#fetchBtn').click();
+ const request = await requestPromise;
+ const headers = request.headers();
+
+ // sampling decision and DSC are continued from the pageload span even after it ended
+ const pageloadTraceId = pageloadTraceContext?.trace_id;
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toEqual(
+ `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true`,
+ );
+ },
+);
+
+sentryTest(
+ 'outgoing fetch request during pageload has pageload traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadEventPromise = getFirstSentryEnvelopeRequest(page);
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.goto(url);
+ await page.locator('#fetchBtn').click();
+ const [pageloadEvent, request] = await Promise.all([pageloadEventPromise, requestPromise]);
+
+ const pageloadTraceContext = pageloadEvent.contexts?.trace;
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(pageloadTraceContext).not.toHaveProperty('parent_span_id');
+
+ const headers = request.headers();
+
+ // sampling decision is propagated from active span sampling decision
+ const pageloadTraceId = pageloadTraceContext?.trace_id;
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toEqual(
+ `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true`,
+ );
+ },
+);
+
+sentryTest(
+ 'outgoing XHR request after pageload has pageload traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url);
+ const pageloadTraceContext = pageloadEvent.contexts?.trace;
+
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(pageloadTraceContext).not.toHaveProperty('parent_span_id');
+
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.locator('#xhrBtn').click();
+ const request = await requestPromise;
+ const headers = request.headers();
+
+ // sampling decision and DSC are continued from the pageload span even after it ended
+ const pageloadTraceId = pageloadTraceContext?.trace_id;
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toEqual(
+ `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true`,
+ );
+ },
+);
+
+sentryTest(
+ 'outgoing XHR request during pageload has pageload traceId in headers',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadEventPromise = getFirstSentryEnvelopeRequest(page);
+ const requestPromise = page.waitForRequest('http://example.com/*');
+ await page.goto(url);
+ await page.locator('#xhrBtn').click();
+ const [pageloadEvent, request] = await Promise.all([pageloadEventPromise, requestPromise]);
+
+ const pageloadTraceContext = pageloadEvent.contexts?.trace;
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(pageloadTraceContext).not.toHaveProperty('parent_span_id');
+
+ const headers = request.headers();
+
+ // sampling decision is propagated from active span sampling decision
+ const pageloadTraceId = pageloadTraceContext?.trace_id;
+ expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`));
+ expect(headers['baggage']).toEqual(
+ `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true`,
+ );
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/subject.js
index 5131ea7631e9..9528f861a723 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/subject.js
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/subject.js
@@ -1,4 +1,16 @@
const errorBtn = document.getElementById('errorBtn');
errorBtn.addEventListener('click', () => {
- throw new Error('Sentry Test Error');
+ throw new Error(`Sentry Test Error ${Math.random()}`);
+});
+
+const fetchBtn = document.getElementById('fetchBtn');
+fetchBtn.addEventListener('click', async () => {
+ await fetch('http://example.com');
+});
+
+const xhrBtn = document.getElementById('xhrBtn');
+xhrBtn.addEventListener('click', () => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', 'http://example.com');
+ xhr.send();
});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/template.html
index a29ad2056a45..a3c17f442605 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/template.html
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/template.html
@@ -5,5 +5,7 @@
+
+