diff --git a/.size-limit.js b/.size-limit.js
index 1a60e556e3e8..5e94a923e656 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -47,6 +47,13 @@ module.exports = [
gzip: true,
limit: '35 KB',
},
+ {
+ name: '@sentry/browser (incl. browserTracingIntegration) - Webpack (gzipped)',
+ path: 'packages/browser/build/npm/esm/index.js',
+ import: '{ init, browserTracingIntegration }',
+ gzip: true,
+ limit: '35 KB',
+ },
{
name: '@sentry/browser (incl. Feedback) - Webpack (gzipped)',
path: 'packages/browser/build/npm/esm/index.js',
diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js
new file mode 100644
index 000000000000..9200b5771ec6
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js
@@ -0,0 +1,23 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+// Replay should not actually work, but still not error out
+window.Replay = new Sentry.replayIntegration({
+ flushMinDelay: 200,
+ flushMaxDelay: 200,
+ minReplayDuration: 0,
+});
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1,
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+ integrations: [window.Replay],
+});
+
+// Ensure none of these break
+window.Replay.start();
+window.Replay.stop();
+window.Replay.flush();
diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html
new file mode 100644
index 000000000000..2b3e2f0b27b4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts
new file mode 100644
index 000000000000..6817367ee68d
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts
@@ -0,0 +1,35 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../utils/fixtures';
+
+sentryTest(
+ 'exports a shim replayIntegration integration for non-replay bundles',
+ async ({ getLocalTestPath, page, forceFlushReplay }) => {
+ const bundle = process.env.PW_BUNDLE;
+
+ if (!bundle || !bundle.startsWith('bundle_') || bundle.includes('replay')) {
+ sentryTest.skip();
+ }
+
+ const consoleMessages: string[] = [];
+ page.on('console', msg => consoleMessages.push(msg.text()));
+
+ let requestCount = 0;
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ requestCount++;
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+ await forceFlushReplay();
+
+ expect(requestCount).toBe(0);
+ expect(consoleMessages).toEqual(['You are using new Replay() even though this bundle does not include replay.']);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js
new file mode 100644
index 000000000000..e5453b648509
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/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({ idleTimeout: 9000 })],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js
new file mode 100644
index 000000000000..5355521f1655
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js
@@ -0,0 +1,11 @@
+document.getElementById('go-background').addEventListener('click', () => {
+ Object.defineProperty(document, 'hidden', { value: true, writable: true });
+ const ev = document.createEvent('Event');
+ ev.initEvent('visibilitychange');
+ document.dispatchEvent(ev);
+});
+
+document.getElementById('start-transaction').addEventListener('click', () => {
+ window.transaction = Sentry.startTransaction({ name: 'test-transaction' });
+ Sentry.getCurrentHub().configureScope(scope => scope.setSpan(window.transaction));
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html
new file mode 100644
index 000000000000..fac45ecebfaf
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts
new file mode 100644
index 000000000000..de1cd552ccab
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts
@@ -0,0 +1,45 @@
+import type { JSHandle } from '@playwright/test';
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+async function getPropertyValue(handle: JSHandle, prop: string) {
+ return (await handle.getProperty(prop))?.jsonValue();
+}
+
+sentryTest('should finish a custom transaction when the page goes background', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadTransaction = await getFirstSentryEnvelopeRequest(page, url);
+ expect(pageloadTransaction).toBeDefined();
+
+ await page.locator('#start-transaction').click();
+ const transactionHandle = await page.evaluateHandle('window.transaction');
+
+ const id_before = await getPropertyValue(transactionHandle, 'span_id');
+ const name_before = await getPropertyValue(transactionHandle, 'name');
+ const status_before = await getPropertyValue(transactionHandle, 'status');
+ const tags_before = await getPropertyValue(transactionHandle, 'tags');
+
+ expect(name_before).toBe('test-transaction');
+ expect(status_before).toBeUndefined();
+ expect(tags_before).toStrictEqual({});
+
+ await page.locator('#go-background').click();
+
+ const id_after = await getPropertyValue(transactionHandle, 'span_id');
+ const name_after = await getPropertyValue(transactionHandle, 'name');
+ const status_after = await getPropertyValue(transactionHandle, 'status');
+ const tags_after = await getPropertyValue(transactionHandle, 'tags');
+
+ expect(id_before).toBe(id_after);
+ expect(name_after).toBe(name_before);
+ expect(status_after).toBe('cancelled');
+ expect(tags_after).toStrictEqual({ visibilitychange: 'document.hidden' });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js
new file mode 100644
index 000000000000..b657f38ac009
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js
@@ -0,0 +1,8 @@
+document.getElementById('go-background').addEventListener('click', () => {
+ setTimeout(() => {
+ Object.defineProperty(document, 'hidden', { value: true, writable: true });
+ const ev = document.createEvent('Event');
+ ev.initEvent('visibilitychange');
+ document.dispatchEvent(ev);
+ }, 250);
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html
new file mode 100644
index 000000000000..31cfc73ec3c3
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts
new file mode 100644
index 000000000000..8432245f9c9b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts
@@ -0,0 +1,23 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should finish pageload transaction when the page goes background', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+ await page.locator('#go-background').click();
+
+ const pageloadTransaction = await getFirstSentryEnvelopeRequest(page);
+
+ expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload');
+ expect(pageloadTransaction.contexts?.trace?.status).toBe('cancelled');
+ expect(pageloadTransaction.contexts?.trace?.tags).toMatchObject({
+ visibilitychange: 'document.hidden',
+ });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js
new file mode 100644
index 000000000000..e32d09a13fab
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({
+ idleTimeout: 1000,
+ _experiments: {
+ enableHTTPTimings: true,
+ },
+ }),
+ ],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts
new file mode 100644
index 000000000000..b6da7522d82c
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts
@@ -0,0 +1,58 @@
+import { expect } from '@playwright/test';
+import type { SerializedEvent } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should create fetch spans with http timing @firefox', async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium', 'firefox'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ sentryTest.skip();
+ }
+ await page.route('http://example.com/*', async route => {
+ const request = route.request();
+ const postData = await request.postDataJSON();
+
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify(Object.assign({ id: 1 }, postData)),
+ });
+ });
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 });
+ const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers
+
+ // eslint-disable-next-line deprecation/deprecation
+ const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client');
+
+ expect(requestSpans).toHaveLength(3);
+
+ await page.pause();
+ requestSpans?.forEach((span, index) =>
+ expect(span).toMatchObject({
+ description: `GET http://example.com/${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: expect.objectContaining({
+ 'http.request.redirect_start': expect.any(Number),
+ 'http.request.fetch_start': expect.any(Number),
+ 'http.request.domain_lookup_start': expect.any(Number),
+ 'http.request.domain_lookup_end': expect.any(Number),
+ 'http.request.connect_start': expect.any(Number),
+ 'http.request.secure_connection_start': expect.any(Number),
+ 'http.request.connection_end': expect.any(Number),
+ 'http.request.request_start': expect.any(Number),
+ 'http.request.response_start': expect.any(Number),
+ 'http.request.response_end': expect.any(Number),
+ 'network.protocol.version': expect.any(String),
+ }),
+ }),
+ );
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js
new file mode 100644
index 000000000000..83076460599f
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/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/browserTracingIntegration/interactions/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js
new file mode 100644
index 000000000000..a37a2c70ad27
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js
@@ -0,0 +1,17 @@
+const delay = e => {
+ const startTime = Date.now();
+
+ function getElasped() {
+ const time = Date.now();
+ return time - startTime;
+ }
+
+ while (getElasped() < 70) {
+ //
+ }
+
+ e.target.classList.add('clicked');
+};
+
+document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay);
+document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js
new file mode 100644
index 000000000000..846538e7f3f0
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/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/browserTracingIntegration/interactions/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html
new file mode 100644
index 000000000000..3357fb20a94e
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Rendered Before Long Task
+
+
+
+
+
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
new file mode 100644
index 000000000000..131403756251
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts
@@ -0,0 +1,114 @@
+import type { Route } from '@playwright/test';
+import { expect } from '@playwright/test';
+import type { Event, Span, SpanContext, Transaction } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import {
+ getFirstSentryEnvelopeRequest,
+ getMultipleSentryEnvelopeRequests,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+type TransactionJSON = ReturnType & {
+ spans: ReturnType[];
+ contexts: SpanContext;
+ platform: string;
+ type: string;
+};
+
+const wait = (time: number) => new Promise(res => setTimeout(res, time));
+
+sentryTest('should capture interaction transaction. @firefox', async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium', 'firefox'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ sentryTest.skip();
+ }
+
+ await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+ await getFirstSentryEnvelopeRequest(page);
+
+ await page.locator('[data-test-id=interaction-button]').click();
+ await page.locator('.clicked[data-test-id=interaction-button]').isVisible();
+
+ const envelopes = await getMultipleSentryEnvelopeRequests(page, 1);
+ expect(envelopes).toHaveLength(1);
+
+ const eventData = envelopes[0];
+
+ expect(eventData.contexts).toMatchObject({ trace: { op: 'ui.action.click' } });
+ expect(eventData.platform).toBe('javascript');
+ expect(eventData.type).toBe('transaction');
+ expect(eventData.spans).toHaveLength(1);
+
+ const interactionSpan = eventData.spans![0];
+ expect(interactionSpan.op).toBe('ui.interaction.click');
+ expect(interactionSpan.description).toBe('body > button.clicked');
+ expect(interactionSpan.timestamp).toBeDefined();
+
+ const interactionSpanDuration = (interactionSpan.timestamp! - interactionSpan.start_timestamp) * 1000;
+ expect(interactionSpanDuration).toBeGreaterThan(70);
+ expect(interactionSpanDuration).toBeLessThan(200);
+});
+
+sentryTest(
+ 'should create only one transaction per interaction @firefox',
+ async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium', 'firefox'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ sentryTest.skip();
+ }
+
+ await page.route('**/path/to/script.js', (route: Route) =>
+ route.fulfill({ path: `${__dirname}/assets/script.js` }),
+ );
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+ await page.goto(url);
+ await getFirstSentryEnvelopeRequest(page);
+
+ for (let i = 0; i < 4; i++) {
+ await wait(100);
+ await page.locator('[data-test-id=interaction-button]').click();
+ const envelope = await getMultipleSentryEnvelopeRequests(page, 1);
+ expect(envelope[0].spans).toHaveLength(1);
+ }
+ },
+);
+
+sentryTest(
+ 'should use the component name for a clicked element when it is available',
+ async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium', 'firefox'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ sentryTest.skip();
+ }
+
+ await page.route('**/path/to/script.js', (route: Route) =>
+ route.fulfill({ path: `${__dirname}/assets/script.js` }),
+ );
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+ await getFirstSentryEnvelopeRequest(page);
+
+ await page.locator('[data-test-id=annotated-button]').click();
+
+ const envelopes = await getMultipleSentryEnvelopeRequests(page, 1);
+ expect(envelopes).toHaveLength(1);
+ const eventData = envelopes[0];
+
+ expect(eventData.spans).toHaveLength(1);
+
+ const interactionSpan = eventData.spans![0];
+ expect(interactionSpan.op).toBe('ui.interaction.click');
+ expect(interactionSpan.description).toBe('body > AnnotatedButton');
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js
new file mode 100644
index 000000000000..9ac3d6fb33d2
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js
@@ -0,0 +1,12 @@
+(() => {
+ const startTime = Date.now();
+
+ function getElasped() {
+ const time = Date.now();
+ return time - startTime;
+ }
+
+ while (getElasped() < 101) {
+ //
+ }
+})();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js
new file mode 100644
index 000000000000..bde12a1304ed
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/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({ enableLongTask: false, idleTimeout: 9000 })],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html
new file mode 100644
index 000000000000..5c3a14114991
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+ Rendered Before Long Task
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts
new file mode 100644
index 000000000000..1f7bb54bb36a
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts
@@ -0,0 +1,23 @@
+import type { Route } from '@playwright/test';
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should not capture long task when flag is disabled.', async ({ browserName, getLocalTestPath, page }) => {
+ // Long tasks only work on chrome
+ if (shouldSkipTracingTest() || browserName !== 'chromium') {
+ sentryTest.skip();
+ }
+
+ await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ // eslint-disable-next-line deprecation/deprecation
+ const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui'));
+
+ expect(uiSpans?.length).toBe(0);
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js
new file mode 100644
index 000000000000..5a2aef02028d
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js
@@ -0,0 +1,12 @@
+(() => {
+ const startTime = Date.now();
+
+ function getElasped() {
+ const time = Date.now();
+ return time - startTime;
+ }
+
+ while (getElasped() < 105) {
+ //
+ }
+})();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js
new file mode 100644
index 000000000000..ad1d8832b228
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({
+ idleTimeout: 9000,
+ }),
+ ],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html
new file mode 100644
index 000000000000..5c3a14114991
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+ Rendered Before Long Task
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts
new file mode 100644
index 000000000000..32819fd784e0
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts
@@ -0,0 +1,38 @@
+import type { Route } from '@playwright/test';
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should capture long task.', async ({ browserName, getLocalTestPath, page }) => {
+ // Long tasks only work on chrome
+ if (shouldSkipTracingTest() || browserName !== 'chromium') {
+ sentryTest.skip();
+ }
+
+ await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ // eslint-disable-next-line deprecation/deprecation
+ const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui'));
+
+ expect(uiSpans?.length).toBeGreaterThan(0);
+
+ const [firstUISpan] = uiSpans || [];
+ expect(firstUISpan).toEqual(
+ expect.objectContaining({
+ op: 'ui.long-task',
+ description: 'Main UI thread blocked',
+ parent_span_id: eventData.contexts?.trace?.span_id,
+ }),
+ );
+ const start = (firstUISpan as Event)['start_timestamp'] ?? 0;
+ const end = (firstUISpan as Event)['timestamp'] ?? 0;
+ const duration = end - start;
+
+ expect(duration).toBeGreaterThanOrEqual(0.1);
+ expect(duration).toBeLessThanOrEqual(0.15);
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js
new file mode 100644
index 000000000000..d4c7810ef518
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/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()],
+ tracesSampleRate: 1,
+ environment: 'staging',
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html
new file mode 100644
index 000000000000..09984cb0c488
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts
new file mode 100644
index 000000000000..ae89fd383cbb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts
@@ -0,0 +1,96 @@
+import { expect } from '@playwright/test';
+import type { Event, EventEnvelopeHeaders } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import {
+ envelopeHeaderRequestParser,
+ getFirstSentryEnvelopeRequest,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+sentryTest(
+ 'should create a pageload transaction based on `sentry-trace` ',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+
+ expect(eventData.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ parent_span_id: '1121201211212012',
+ trace_id: '12312012123120121231201212312012',
+ });
+
+ expect(eventData.spans?.length).toBeGreaterThan(0);
+ },
+);
+
+sentryTest(
+ 'should pick up `baggage` tag, propagate the content in transaction and not add own data',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ 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',
+ });
+ },
+);
+
+sentryTest(
+ "should create a navigation that's not influenced by `sentry-trace` ",
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadRequest = await getFirstSentryEnvelopeRequest(page, url);
+ const navigationRequest = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
+
+ expect(pageloadRequest.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ parent_span_id: '1121201211212012',
+ trace_id: '12312012123120121231201212312012',
+ });
+
+ 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/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts
new file mode 100644
index 000000000000..5a46a65a4392
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts
@@ -0,0 +1,51 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should create a navigation transaction on page navigation', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadRequest = await getFirstSentryEnvelopeRequest(page, url);
+ const navigationRequest = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
+
+ expect(pageloadRequest.contexts?.trace?.op).toBe('pageload');
+ expect(navigationRequest.contexts?.trace?.op).toBe('navigation');
+
+ expect(navigationRequest.transaction_info?.source).toEqual('url');
+
+ const pageloadTraceId = pageloadRequest.contexts?.trace?.trace_id;
+ const navigationTraceId = navigationRequest.contexts?.trace?.trace_id;
+
+ expect(pageloadTraceId).toBeDefined();
+ expect(navigationTraceId).toBeDefined();
+ expect(pageloadTraceId).not.toEqual(navigationTraceId);
+
+ 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,
+ }),
+ );
+
+ expect(pageloadSpanId).not.toEqual(navigationSpanId);
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js
new file mode 100644
index 000000000000..1f0b64911a75
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window._testBaseTimestamp = performance.timeOrigin / 1000;
+
+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/browserTracingIntegration/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts
new file mode 100644
index 000000000000..6a186b63b02a
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts
@@ -0,0 +1,24 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should create a pageload transaction', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ const timeOrigin = await page.evaluate('window._testBaseTimestamp');
+
+ const { start_timestamp: startTimestamp } = eventData;
+
+ expect(startTimestamp).toBeCloseTo(timeOrigin, 1);
+
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+ expect(eventData.spans?.length).toBeGreaterThan(0);
+ expect(eventData.transaction_info?.source).toEqual('url');
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js
new file mode 100644
index 000000000000..2c5a44a7f76d
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window._testBaseTimestamp = performance.timeOrigin / 1000;
+
+setTimeout(() => {
+ window._testTimeoutTimestamp = (performance.timeOrigin + performance.now()) / 1000;
+ Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+ });
+}, 250);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts
new file mode 100644
index 000000000000..882c08d23c5e
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts
@@ -0,0 +1,26 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should create a pageload transaction when initialized delayed', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ const timeOrigin = await page.evaluate('window._testBaseTimestamp');
+ const timeoutTimestamp = await page.evaluate('window._testTimeoutTimestamp');
+
+ const { start_timestamp: startTimestamp } = eventData;
+
+ expect(startTimestamp).toBeCloseTo(timeOrigin, 1);
+ expect(startTimestamp).toBeLessThan(timeoutTimestamp);
+
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+ expect(eventData.spans?.length).toBeGreaterThan(0);
+ expect(eventData.transaction_info?.source).toEqual('url');
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js
new file mode 100644
index 000000000000..8b12fe807d7b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/browser';
+import { startSpanManual } from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+});
+
+setTimeout(() => {
+ startSpanManual({ name: 'pageload-child-span' }, () => {});
+}, 200);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts
new file mode 100644
index 000000000000..dbb284aecb3b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts
@@ -0,0 +1,27 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+// This tests asserts that the pageload transaction will finish itself after about 15 seconds (3x5s of heartbeats) if it
+// has a child span without adding any additional ones or finishing any of them finishing. All of the child spans that
+// are still running should have the status "cancelled".
+sentryTest(
+ 'should send a pageload transaction terminated via heartbeat timeout',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+ expect(
+ // eslint-disable-next-line deprecation/deprecation
+ eventData.spans?.find(span => span.description === 'pageload-child-span' && span.status === 'cancelled'),
+ ).toBeDefined();
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js
new file mode 100644
index 000000000000..ad48a291386e
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/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({ tracePropagationTargets: ['http://example.com'] })],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts
new file mode 100644
index 000000000000..fb6e9e540c46
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts
@@ -0,0 +1,33 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
+
+sentryTest(
+ 'should attach `sentry-trace` and `baggage` header to request matching tracePropagationTargets',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ for (const request of requests) {
+ const requestHeaders = request.headers();
+
+ expect(requestHeaders).toMatchObject({
+ 'sentry-trace': expect.any(String),
+ baggage: expect.any(String),
+ });
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js
new file mode 100644
index 000000000000..572b8c69d4dc
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js
@@ -0,0 +1,11 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({ tracePropagationTargets: [], tracingOrigins: ['http://example.com'] }),
+ ],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts
new file mode 100644
index 000000000000..a6cc58ca46ff
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts
@@ -0,0 +1,32 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
+
+sentryTest(
+ '[pre-v8] should prefer custom tracePropagationTargets over tracingOrigins',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ for (const request of requests) {
+ const requestHeaders = request.headers();
+ expect(requestHeaders).not.toMatchObject({
+ 'sentry-trace': expect.any(String),
+ baggage: expect.any(String),
+ });
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/init.js
new file mode 100644
index 000000000000..45e5237e4c24
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/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({ tracingOrigins: ['http://example.com'] })],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts
new file mode 100644
index 000000000000..9f32b7b1ad28
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts
@@ -0,0 +1,32 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
+
+sentryTest(
+ '[pre-v8] should attach `sentry-trace` and `baggage` header to request matching tracingOrigins',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ for (const request of requests) {
+ const requestHeaders = request.headers();
+ expect(requestHeaders).toMatchObject({
+ 'sentry-trace': expect.any(String),
+ baggage: expect.any(String),
+ });
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js
new file mode 100644
index 000000000000..83076460599f
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/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/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js
new file mode 100644
index 000000000000..4e9cf0d01004
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js
@@ -0,0 +1 @@
+fetch('http://localhost:4200/0').then(fetch('http://localhost:4200/1').then(fetch('http://localhost:4200/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts
new file mode 100644
index 000000000000..120b36ec88db
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts
@@ -0,0 +1,32 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
+
+sentryTest(
+ 'should attach `sentry-trace` and `baggage` header to request matching default tracePropagationTargets',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://localhost:4200/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ for (const request of requests) {
+ const requestHeaders = request.headers();
+ expect(requestHeaders).toMatchObject({
+ 'sentry-trace': expect.any(String),
+ baggage: expect.any(String),
+ });
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js
new file mode 100644
index 000000000000..83076460599f
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/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/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts
new file mode 100644
index 000000000000..116319259101
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts
@@ -0,0 +1,32 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
+
+sentryTest(
+ 'should not attach `sentry-trace` and `baggage` header to request not matching default tracePropagationTargets',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ for (const request of requests) {
+ const requestHeaders = request.headers();
+ expect(requestHeaders).not.toMatchObject({
+ 'sentry-trace': expect.any(String),
+ baggage: expect.any(String),
+ });
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/init.js
new file mode 100644
index 000000000000..cd05f29615bb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/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',
+ sampleRate: 1,
+ integrations: [new Sentry.Integrations.BrowserTracing()],
+});
+
+// This should not fail
+Sentry.addTracingExtensions();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html
new file mode 100644
index 000000000000..2b3e2f0b27b4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts
new file mode 100644
index 000000000000..e37181ee815b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts
@@ -0,0 +1,36 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../utils/helpers';
+
+sentryTest(
+ 'exports a shim Integrations.BrowserTracing integration for non-tracing bundles',
+ async ({ getLocalTestPath, page }) => {
+ // Skip in tracing tests
+ if (!shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const consoleMessages: string[] = [];
+ page.on('console', msg => consoleMessages.push(msg.text()));
+
+ let requestCount = 0;
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ requestCount++;
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+
+ expect(requestCount).toBe(0);
+ expect(consoleMessages).toEqual([
+ 'You are using new BrowserTracing() even though this bundle does not include tracing.',
+ ]);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js
index cd05f29615bb..e8ba5702cff8 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js
@@ -5,7 +5,7 @@ window.Sentry = Sentry;
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 1,
- integrations: [new Sentry.Integrations.BrowserTracing()],
+ integrations: [new Sentry.browserTracingIntegration()],
});
// This should not fail
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts
index e37181ee815b..71510468a513 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts
@@ -4,7 +4,7 @@ import { sentryTest } from '../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../utils/helpers';
sentryTest(
- 'exports a shim Integrations.BrowserTracing integration for non-tracing bundles',
+ 'exports a shim browserTracingIntegration() integration for non-tracing bundles',
async ({ getLocalTestPath, page }) => {
// Skip in tracing tests
if (!shouldSkipTracingTest()) {
diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts
index 2e10d4210953..3960c25eccd3 100644
--- a/packages/astro/test/client/sdk.test.ts
+++ b/packages/astro/test/client/sdk.test.ts
@@ -1,5 +1,6 @@
import type { BrowserClient } from '@sentry/browser';
-import { getCurrentScope } from '@sentry/browser';
+import { getActiveSpan } from '@sentry/browser';
+import { browserTracingIntegration, getCurrentScope } from '@sentry/browser';
import * as SentryBrowser from '@sentry/browser';
import { BrowserTracing, SDK_VERSION, WINDOW, getClient } from '@sentry/browser';
import { vi } from 'vitest';
@@ -100,7 +101,7 @@ describe('Sentry client SDK', () => {
delete globalThis.__SENTRY_TRACING__;
});
- it('Overrides the automatically default BrowserTracing instance with a a user-provided instance', () => {
+ it('Overrides the automatically default BrowserTracing instance with a a user-provided BrowserTracing instance', () => {
init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })],
@@ -118,6 +119,22 @@ describe('Sentry client SDK', () => {
// This shows that the user-configured options are still here
expect(options.finalTimeout).toEqual(10);
});
+
+ it('Overrides the automatically default BrowserTracing instance with a a user-provided browserTracingIntergation instance', () => {
+ init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ browserTracingIntegration({ finalTimeout: 10, instrumentNavigation: false, instrumentPageLoad: false }),
+ ],
+ enableTracing: true,
+ });
+
+ const browserTracing = getClient()?.getIntegrationByName('BrowserTracing');
+ expect(browserTracing).toBeDefined();
+
+ // no active span means the settings were respected
+ expect(getActiveSpan()).toBeUndefined();
+ });
});
});
});
diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts
index 1bc58b780748..5fff014eaa8d 100644
--- a/packages/browser/src/helpers.ts
+++ b/packages/browser/src/helpers.ts
@@ -1,5 +1,7 @@
+import type { browserTracingIntegration } from '@sentry-internal/tracing';
+import { BrowserTracing } from '@sentry-internal/tracing';
import { captureException, withScope } from '@sentry/core';
-import type { DsnLike, Mechanism, WrappedFunction } from '@sentry/types';
+import type { DsnLike, Integration, Mechanism, WrappedFunction } from '@sentry/types';
import {
GLOBAL_OBJ,
addExceptionMechanism,
@@ -185,3 +187,33 @@ export interface ReportDialogOptions {
/** Callback after reportDialog closed */
onClose?(this: void): void;
}
+
+/**
+ * This is a slim shim of `browserTracingIntegration` for the CDN bundles.
+ * Since the actual functional integration uses a different code from `BrowserTracing`,
+ * we want to avoid shipping both of them in the CDN bundles, as that would blow up the size.
+ * Instead, we provide a functional integration with the same API, but the old implementation.
+ * This means that it's not possible to register custom routing instrumentation, but that's OK for now.
+ * We also don't expose the utilities for this anyhow in the CDN bundles.
+ * For users that need custom routing in CDN bundles, they have to continue using `new BrowserTracing()` until v8.
+ */
+export function bundleBrowserTracingIntegration(
+ options: Parameters[0] = {},
+): Integration {
+ // Migrate some options from the old integration to the new one
+ const opts: ConstructorParameters[0] = options;
+
+ if (typeof options.markBackgroundSpan === 'boolean') {
+ opts.markBackgroundTransactions = options.markBackgroundSpan;
+ }
+
+ if (typeof options.instrumentPageLoad === 'boolean') {
+ opts.startTransactionOnPageLoad = options.instrumentPageLoad;
+ }
+
+ if (typeof options.instrumentNavigation === 'boolean') {
+ opts.startTransactionOnLocationChange = options.instrumentNavigation;
+ }
+
+ return new BrowserTracing(opts);
+}
diff --git a/packages/browser/src/index.bundle.feedback.ts b/packages/browser/src/index.bundle.feedback.ts
index 5d3612106286..af4de5ea063d 100644
--- a/packages/browser/src/index.bundle.feedback.ts
+++ b/packages/browser/src/index.bundle.feedback.ts
@@ -1,6 +1,12 @@
// This is exported so the loader does not fail when switching off Replay/Tracing
import { Feedback, feedbackIntegration } from '@sentry-internal/feedback';
-import { BrowserTracing, Replay, addTracingExtensions, replayIntegration } from '@sentry-internal/integration-shims';
+import {
+ BrowserTracing,
+ Replay,
+ addTracingExtensions,
+ browserTracingIntegration,
+ replayIntegration,
+} from '@sentry-internal/integration-shims';
import * as Sentry from './index.bundle.base';
@@ -13,6 +19,7 @@ Sentry.Integrations.BrowserTracing = BrowserTracing;
export * from './index.bundle.base';
export {
BrowserTracing,
+ browserTracingIntegration,
addTracingExtensions,
// eslint-disable-next-line deprecation/deprecation
Replay,
diff --git a/packages/browser/src/index.bundle.replay.ts b/packages/browser/src/index.bundle.replay.ts
index 2609e7d9b48c..175a435fadcf 100644
--- a/packages/browser/src/index.bundle.replay.ts
+++ b/packages/browser/src/index.bundle.replay.ts
@@ -3,6 +3,7 @@ import {
BrowserTracing,
Feedback,
addTracingExtensions,
+ browserTracingIntegration,
feedbackIntegration,
} from '@sentry-internal/integration-shims';
import { Replay, replayIntegration } from '@sentry/replay';
@@ -18,6 +19,7 @@ Sentry.Integrations.BrowserTracing = BrowserTracing;
export * from './index.bundle.base';
export {
BrowserTracing,
+ browserTracingIntegration,
addTracingExtensions,
// eslint-disable-next-line deprecation/deprecation
Replay,
diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts
index e17c7de4159a..df151bba0a8f 100644
--- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts
+++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts
@@ -1,6 +1,7 @@
import { Feedback, feedbackIntegration } from '@sentry-internal/feedback';
import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing';
import { Replay, replayIntegration } from '@sentry/replay';
+import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers';
import * as Sentry from './index.bundle.base';
@@ -23,6 +24,7 @@ export {
feedbackIntegration,
replayIntegration,
BrowserTracing,
+ browserTracingIntegration,
Span,
addExtensionMethods,
};
diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts
index 5dc0537be064..2437a8546d5c 100644
--- a/packages/browser/src/index.bundle.tracing.replay.ts
+++ b/packages/browser/src/index.bundle.tracing.replay.ts
@@ -1,6 +1,7 @@
import { Feedback, feedbackIntegration } from '@sentry-internal/integration-shims';
import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing';
import { Replay, replayIntegration } from '@sentry/replay';
+import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers';
import * as Sentry from './index.bundle.base';
@@ -23,6 +24,7 @@ export {
replayIntegration,
feedbackIntegration,
BrowserTracing,
+ browserTracingIntegration,
Span,
addExtensionMethods,
};
diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts
index f810b61b92a7..2ca0613146f0 100644
--- a/packages/browser/src/index.bundle.tracing.ts
+++ b/packages/browser/src/index.bundle.tracing.ts
@@ -1,6 +1,7 @@
// This is exported so the loader does not fail when switching off Replay
import { Feedback, Replay, feedbackIntegration, replayIntegration } from '@sentry-internal/integration-shims';
import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing';
+import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers';
import * as Sentry from './index.bundle.base';
@@ -23,6 +24,7 @@ export {
feedbackIntegration,
replayIntegration,
BrowserTracing,
+ browserTracingIntegration,
Span,
addExtensionMethods,
};
diff --git a/packages/browser/src/index.bundle.ts b/packages/browser/src/index.bundle.ts
index a92ff6bf66ec..93a0b0cb498a 100644
--- a/packages/browser/src/index.bundle.ts
+++ b/packages/browser/src/index.bundle.ts
@@ -4,6 +4,7 @@ import {
Feedback,
Replay,
addTracingExtensions,
+ browserTracingIntegration,
feedbackIntegration,
replayIntegration,
} from '@sentry-internal/integration-shims';
@@ -24,6 +25,7 @@ export {
Replay,
// eslint-disable-next-line deprecation/deprecation
Feedback,
+ browserTracingIntegration,
feedbackIntegration,
replayIntegration,
};
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 19c377fc5931..0c75bae6e1f9 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -57,6 +57,11 @@ export {
BrowserTracing,
defaultRequestInstrumentationOptions,
instrumentOutgoingRequests,
+ browserTracingIntegration,
+ startBrowserTracingNavigationSpan,
+ startBrowserTracingPageLoadSpan,
+ disableDefaultBrowserTracingNavigationSpan,
+ disableDefaultBrowserTracingPageLoadSpan,
} from '@sentry-internal/tracing';
export type { RequestInstrumentationOptions } from '@sentry-internal/tracing';
export {
diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts
index c7736e278c1c..a4d43fc58a8a 100644
--- a/packages/core/src/baseclient.ts
+++ b/packages/core/src/baseclient.ts
@@ -26,6 +26,7 @@ import type {
SessionAggregates,
Severity,
SeverityLevel,
+ StartSpanOptions,
Transaction,
TransactionEvent,
Transport,
@@ -482,6 +483,12 @@ export abstract class BaseClient implements Client {
callback: (feedback: FeedbackEvent, options?: { includeReplay: boolean }) => void,
): void;
+ /** @inheritdoc */
+ public on(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void;
+
+ /** @inheritdoc */
+ public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void;
+
/** @inheritdoc */
public on(hook: string, callback: unknown): void {
if (!this._hooks[hook]) {
@@ -522,6 +529,12 @@ export abstract class BaseClient implements Client {
/** @inheritdoc */
public emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay: boolean }): void;
+ /** @inheritdoc */
+ public emit(hook: 'startPageLoadSpan', options: StartSpanOptions): void;
+
+ /** @inheritdoc */
+ public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void;
+
/** @inheritdoc */
public emit(hook: string, ...rest: unknown[]): void {
if (this._hooks[hook]) {
diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts
index bb92373d3e58..885cbd7c9d08 100644
--- a/packages/core/src/tracing/trace.ts
+++ b/packages/core/src/tracing/trace.ts
@@ -1,15 +1,5 @@
-import type {
- Instrumenter,
- Primitive,
- Scope,
- Span,
- SpanTimeInput,
- TransactionContext,
- TransactionMetadata,
-} from '@sentry/types';
-import type { SpanAttributes } from '@sentry/types';
-import type { SpanOrigin } from '@sentry/types';
-import type { TransactionSource } from '@sentry/types';
+import type { Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types';
+
import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build';
@@ -20,109 +10,6 @@ import { handleCallbackErrors } from '../utils/handleCallbackErrors';
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
import { spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
-interface StartSpanOptions extends Omit {
- /** A manually specified start time for the created `Span` object. */
- startTime?: SpanTimeInput;
-
- /** If defined, start this span off this scope instead off the current scope. */
- scope?: Scope;
-
- /** The name of the span. */
- name: string;
-
- /** An op for the span. This is a categorization for spans. */
- op?: string;
-
- /**
- * The origin of the span - if it comes from auto instrumentation or manual instrumentation.
- *
- * @deprecated Set `attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]` instead.
- */
- origin?: SpanOrigin;
-
- /** Attributes for the span. */
- attributes?: SpanAttributes;
-
- // All remaining fields are deprecated
-
- /**
- * @deprecated Manually set the end timestamp instead.
- */
- trimEnd?: boolean;
-
- /**
- * @deprecated This cannot be set manually anymore.
- */
- parentSampled?: boolean;
-
- /**
- * @deprecated Use attributes or set data on scopes instead.
- */
- metadata?: Partial;
-
- /**
- * The name thingy.
- * @deprecated Use `name` instead.
- */
- description?: string;
-
- /**
- * @deprecated Use `span.setStatus()` instead.
- */
- status?: string;
-
- /**
- * @deprecated Use `scope` instead.
- */
- parentSpanId?: string;
-
- /**
- * @deprecated You cannot manually set the span to sampled anymore.
- */
- sampled?: boolean;
-
- /**
- * @deprecated You cannot manually set the spanId anymore.
- */
- spanId?: string;
-
- /**
- * @deprecated You cannot manually set the traceId anymore.
- */
- traceId?: string;
-
- /**
- * @deprecated Use an attribute instead.
- */
- source?: TransactionSource;
-
- /**
- * @deprecated Use attributes or set tags on the scope instead.
- */
- tags?: { [key: string]: Primitive };
-
- /**
- * @deprecated Use attributes instead.
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- data?: { [key: string]: any };
-
- /**
- * @deprecated Use `startTime` instead.
- */
- startTimestamp?: number;
-
- /**
- * @deprecated Use `span.end()` instead.
- */
- endTimestamp?: number;
-
- /**
- * @deprecated You cannot set the instrumenter manually anymore.
- */
- instrumenter?: Instrumenter;
-}
-
/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
*
diff --git a/packages/integration-shims/src/BrowserTracing.ts b/packages/integration-shims/src/BrowserTracing.ts
index 310dc589afe9..8e3d61bae58f 100644
--- a/packages/integration-shims/src/BrowserTracing.ts
+++ b/packages/integration-shims/src/BrowserTracing.ts
@@ -33,7 +33,16 @@ class BrowserTracingShim implements Integration {
}
}
-export { BrowserTracingShim as BrowserTracing };
+/**
+ * This is a shim for the BrowserTracing integration.
+ * It is needed in order for the CDN bundles to continue working when users add/remove tracing
+ * from it, without changing their config. This is necessary for the loader mechanism.
+ */
+function browserTracingIntegrationShim(_options: unknown): Integration {
+ return new BrowserTracingShim({});
+}
+
+export { BrowserTracingShim as BrowserTracing, browserTracingIntegrationShim as browserTracingIntegration };
/** Shim function */
export function addTracingExtensions(): void {
diff --git a/packages/integration-shims/src/index.ts b/packages/integration-shims/src/index.ts
index 43243f69a194..bffdf82c99f7 100644
--- a/packages/integration-shims/src/index.ts
+++ b/packages/integration-shims/src/index.ts
@@ -3,9 +3,16 @@ export {
Feedback,
feedbackIntegration,
} from './Feedback';
+
export {
// eslint-disable-next-line deprecation/deprecation
Replay,
replayIntegration,
} from './Replay';
-export { BrowserTracing, addTracingExtensions } from './BrowserTracing';
+
+export {
+ // eslint-disable-next-line deprecation/deprecation
+ BrowserTracing,
+ browserTracingIntegration,
+ addTracingExtensions,
+} from './BrowserTracing';
diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts
index d1d5e1db7ff5..a1c20937f578 100644
--- a/packages/nextjs/src/client/index.ts
+++ b/packages/nextjs/src/client/index.ts
@@ -1,5 +1,5 @@
import { applySdkMetadata, hasTracingEnabled } from '@sentry/core';
-import type { BrowserOptions } from '@sentry/react';
+import type { BrowserOptions, browserTracingIntegration } from '@sentry/react';
import {
Integrations as OriginalIntegrations,
getCurrentScope,
@@ -86,13 +86,30 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void {
}
}
+function isNewBrowserTracingIntegration(
+ integration: Integration,
+): integration is Integration & { options?: Parameters[0] } {
+ return !!integration.afterAllSetup && !!(integration as BrowserTracing).options;
+}
+
function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] {
const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing');
+
+ if (!browserTracing) {
+ return integrations;
+ }
+
+ // If `browserTracingIntegration()` was added, we need to force-convert it to our custom one
+ if (isNewBrowserTracingIntegration(browserTracing)) {
+ const { options } = browserTracing;
+ integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options);
+ }
+
// If BrowserTracing was added, but it is not our forked version,
// replace it with our forked version with the same options
- if (browserTracing && !(browserTracing instanceof BrowserTracing)) {
+ if (!(browserTracing instanceof BrowserTracing)) {
const options: ConstructorParameters[0] = (browserTracing as BrowserTracing).options;
- // These two options are overwritten by the custom integration
+ // This option is overwritten by the custom integration
delete options.routingInstrumentation;
// eslint-disable-next-line deprecation/deprecation
delete options.tracingOrigins;
diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts
index 464b7db14dc7..f4ec99c3cc71 100644
--- a/packages/nextjs/test/clientSdk.test.ts
+++ b/packages/nextjs/test/clientSdk.test.ts
@@ -1,6 +1,7 @@
import { BaseClient } from '@sentry/core';
import * as SentryReact from '@sentry/react';
import type { BrowserClient } from '@sentry/react';
+import { browserTracingIntegration } from '@sentry/react';
import { WINDOW, getClient, getCurrentScope } from '@sentry/react';
import type { Integration } from '@sentry/types';
import { logger } from '@sentry/utils';
@@ -166,7 +167,7 @@ describe('Client init()', () => {
init({
dsn: TEST_DSN,
tracesSampleRate: 1.0,
- integrations: [new BrowserTracing({ startTransactionOnLocationChange: false })],
+ integrations: [new BrowserTracing({ finalTimeout: 10 })],
});
const client = getClient()!;
@@ -177,7 +178,27 @@ describe('Client init()', () => {
expect.objectContaining({
routingInstrumentation: nextRouterInstrumentation,
// This proves it's still the user's copy
- startTransactionOnLocationChange: false,
+ finalTimeout: 10,
+ }),
+ );
+ });
+
+ it('forces correct router instrumentation if user provides `browserTracingIntegration`', () => {
+ init({
+ dsn: TEST_DSN,
+ integrations: [browserTracingIntegration({ finalTimeout: 10 })],
+ enableTracing: true,
+ });
+
+ const client = getClient()!;
+ const integration = client.getIntegrationByName('BrowserTracing');
+
+ expect(integration).toBeDefined();
+ expect(integration?.options).toEqual(
+ expect.objectContaining({
+ routingInstrumentation: nextRouterInstrumentation,
+ // This proves it's still the user's copy
+ finalTimeout: 10,
}),
);
});
diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts
index 7b9c608a862d..920b2db75193 100644
--- a/packages/sveltekit/src/client/sdk.ts
+++ b/packages/sveltekit/src/client/sdk.ts
@@ -1,5 +1,5 @@
import { applySdkMetadata, hasTracingEnabled } from '@sentry/core';
-import type { BrowserOptions } from '@sentry/svelte';
+import type { BrowserOptions, browserTracingIntegration } from '@sentry/svelte';
import { getDefaultIntegrations as getDefaultSvelteIntegrations } from '@sentry/svelte';
import { WINDOW, getCurrentScope, init as initSvelteSdk } from '@sentry/svelte';
import type { Integration } from '@sentry/types';
@@ -61,11 +61,28 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void {
}
}
+function isNewBrowserTracingIntegration(
+ integration: Integration,
+): integration is Integration & { options?: Parameters[0] } {
+ return !!integration.afterAllSetup && !!(integration as BrowserTracing).options;
+}
+
function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] {
const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing');
+
+ if (!browserTracing) {
+ return integrations;
+ }
+
+ // If `browserTracingIntegration()` was added, we need to force-convert it to our custom one
+ if (isNewBrowserTracingIntegration(browserTracing)) {
+ const { options } = browserTracing;
+ integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options);
+ }
+
// If BrowserTracing was added, but it is not our forked version,
// replace it with our forked version with the same options
- if (browserTracing && !(browserTracing instanceof BrowserTracing)) {
+ if (!(browserTracing instanceof BrowserTracing)) {
const options: ConstructorParameters[0] = (browserTracing as BrowserTracing).options;
// This option is overwritten by the custom integration
delete options.routingInstrumentation;
diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts
index 10292658bc54..4b0afb85bcd8 100644
--- a/packages/sveltekit/test/client/sdk.test.ts
+++ b/packages/sveltekit/test/client/sdk.test.ts
@@ -1,7 +1,7 @@
import { getClient, getCurrentScope } from '@sentry/core';
import type { BrowserClient } from '@sentry/svelte';
import * as SentrySvelte from '@sentry/svelte';
-import { SDK_VERSION, WINDOW } from '@sentry/svelte';
+import { SDK_VERSION, WINDOW, browserTracingIntegration } from '@sentry/svelte';
import { vi } from 'vitest';
import { BrowserTracing, init } from '../../src/client';
@@ -100,7 +100,26 @@ describe('Sentry client SDK', () => {
it('Merges a user-provided BrowserTracing integration with the automatically added one', () => {
init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
- integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })],
+ integrations: [new BrowserTracing({ finalTimeout: 10 })],
+ enableTracing: true,
+ });
+
+ const browserTracing = getClient()?.getIntegrationByName('BrowserTracing') as BrowserTracing;
+ const options = browserTracing.options;
+
+ expect(browserTracing).toBeDefined();
+
+ // This shows that the user-configured options are still here
+ expect(options.finalTimeout).toEqual(10);
+
+ // But we force the routing instrumentation to be ours
+ expect(options.routingInstrumentation).toEqual(svelteKitRoutingInstrumentation);
+ });
+
+ it('Merges a user-provided browserTracingIntegration with the automatically added one', () => {
+ init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [browserTracingIntegration({ finalTimeout: 10 })],
enableTracing: true,
});
diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts
new file mode 100644
index 000000000000..8184ad058039
--- /dev/null
+++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts
@@ -0,0 +1,549 @@
+/* eslint-disable max-lines, complexity */
+import type { IdleTransaction } from '@sentry/core';
+import { getClient } from '@sentry/core';
+import { defineIntegration, getCurrentHub } from '@sentry/core';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ TRACING_DEFAULTS,
+ addTracingExtensions,
+ getActiveTransaction,
+ spanIsSampled,
+ spanToJSON,
+ startIdleTransaction,
+} from '@sentry/core';
+import type {
+ IntegrationFn,
+ StartSpanOptions,
+ Transaction,
+ TransactionContext,
+ TransactionSource,
+} from '@sentry/types';
+import type { Span } from '@sentry/types';
+import {
+ addHistoryInstrumentationHandler,
+ browserPerformanceTimeOrigin,
+ getDomElement,
+ logger,
+ tracingContextFromHeaders,
+} from '@sentry/utils';
+
+import { DEBUG_BUILD } from '../common/debug-build';
+import { registerBackgroundTabDetection } from './backgroundtab';
+import {
+ addPerformanceEntries,
+ startTrackingInteractions,
+ startTrackingLongTasks,
+ startTrackingWebVitals,
+} from './metrics';
+import type { RequestInstrumentationOptions } from './request';
+import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request';
+import { WINDOW } from './types';
+
+export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
+
+/** Options for Browser Tracing integration */
+export interface BrowserTracingOptions extends RequestInstrumentationOptions {
+ /**
+ * The time to wait in ms until the transaction will be finished during an idle state. An idle state is defined
+ * by a moment where there are no in-progress spans.
+ *
+ * The transaction will use the end timestamp of the last finished span as the endtime for the transaction.
+ * If there are still active spans when this the `idleTimeout` is set, the `idleTimeout` will get reset.
+ * Time is in ms.
+ *
+ * Default: 1000
+ */
+ idleTimeout: number;
+
+ /**
+ * The max duration for a transaction. If a transaction duration hits the `finalTimeout` value, it
+ * will be finished.
+ * Time is in ms.
+ *
+ * Default: 30000
+ */
+ finalTimeout: number;
+
+ /**
+ * The heartbeat interval. If no new spans are started or open spans are finished within 3 heartbeats,
+ * the transaction will be finished.
+ * Time is in ms.
+ *
+ * Default: 5000
+ */
+ heartbeatInterval: number;
+
+ /**
+ * If a span should be created on page load.
+ * Default: true
+ */
+ instrumentPageLoad: boolean;
+
+ /**
+ * If a span should be created on navigation (history change).
+ * Default: true
+ */
+ instrumentNavigation: boolean;
+
+ /**
+ * Flag spans where tabs moved to background with "cancelled". Browser background tab timing is
+ * not suited towards doing precise measurements of operations. By default, we recommend that this option
+ * be enabled as background transactions can mess up your statistics in nondeterministic ways.
+ *
+ * Default: true
+ */
+ markBackgroundSpan: boolean;
+
+ /**
+ * If true, Sentry will capture long tasks and add them to the corresponding transaction.
+ *
+ * Default: true
+ */
+ enableLongTask: boolean;
+
+ /**
+ * _metricOptions allows the user to send options to change how metrics are collected.
+ *
+ * _metricOptions is currently experimental.
+ *
+ * Default: undefined
+ */
+ _metricOptions?: Partial<{
+ /**
+ * @deprecated This property no longer has any effect and will be removed in v8.
+ */
+ _reportAllChanges: boolean;
+ }>;
+
+ /**
+ * _experiments allows the user to send options to define how this integration works.
+ * Note that the `enableLongTask` options is deprecated in favor of the option at the top level, and will be removed in v8.
+ *
+ * TODO (v8): Remove enableLongTask
+ *
+ * Default: undefined
+ */
+ _experiments: Partial<{
+ enableInteractions: boolean;
+ }>;
+
+ /**
+ * A callback which is called before a span for a pageload or navigation is started.
+ * It receives the options passed to `startSpan`, and expects to return an updated options object.
+ */
+ beforeStartSpan?: (options: StartSpanOptions) => StartSpanOptions;
+}
+
+const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
+ ...TRACING_DEFAULTS,
+ instrumentNavigation: true,
+ instrumentPageLoad: true,
+ markBackgroundSpan: true,
+ enableLongTask: true,
+ _experiments: {},
+ ...defaultRequestInstrumentationOptions,
+};
+
+let shouldUseDefaultPageLoadSpan = true;
+let shouldUseDefaultNavigationSpan = true;
+
+/**
+ * The Browser Tracing integration automatically instruments browser pageload/navigation
+ * actions as transactions, and captures requests, metrics and errors as spans.
+ *
+ * The integration can be configured with a variety of options, and can be extended to use
+ * any routing library. This integration uses {@see IdleTransaction} to create transactions.
+ */
+export const _browserTracingIntegration = ((_options: Partial = {}) => {
+ const _hasSetTracePropagationTargets = DEBUG_BUILD
+ ? !!(
+ // eslint-disable-next-line deprecation/deprecation
+ (_options.tracePropagationTargets || _options.tracingOrigins)
+ )
+ : false;
+
+ addTracingExtensions();
+
+ // TODO (v8): remove this block after tracingOrigins is removed
+ // Set tracePropagationTargets to tracingOrigins if specified by the user
+ // In case both are specified, tracePropagationTargets takes precedence
+ // eslint-disable-next-line deprecation/deprecation
+ if (!_options.tracePropagationTargets && _options.tracingOrigins) {
+ // eslint-disable-next-line deprecation/deprecation
+ _options.tracePropagationTargets = _options.tracingOrigins;
+ }
+
+ const options = {
+ ...DEFAULT_BROWSER_TRACING_OPTIONS,
+ ..._options,
+ };
+
+ const _collectWebVitals = startTrackingWebVitals();
+
+ if (options.enableLongTask) {
+ startTrackingLongTasks();
+ }
+ if (options._experiments.enableInteractions) {
+ startTrackingInteractions();
+ }
+
+ let latestRouteName: string | undefined;
+ let latestRouteSource: TransactionSource | undefined;
+
+ /** Create routing idle transaction. */
+ function _createRouteTransaction(context: TransactionContext): Transaction | undefined {
+ // eslint-disable-next-line deprecation/deprecation
+ const hub = getCurrentHub();
+
+ const { beforeStartSpan, idleTimeout, finalTimeout, heartbeatInterval } = options;
+
+ const isPageloadTransaction = context.op === 'pageload';
+
+ const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : '';
+ const baggage = isPageloadTransaction ? getMetaContent('baggage') : '';
+ const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
+ sentryTrace,
+ baggage,
+ );
+
+ const expandedContext: TransactionContext = {
+ ...context,
+ ...traceparentData,
+ metadata: {
+ // eslint-disable-next-line deprecation/deprecation
+ ...context.metadata,
+ dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
+ },
+ trimEnd: true,
+ };
+
+ const finalContext = beforeStartSpan ? beforeStartSpan(expandedContext) : expandedContext;
+
+ // If `beforeStartSpan` set a custom name, record that fact
+ // eslint-disable-next-line deprecation/deprecation
+ finalContext.metadata =
+ finalContext.name !== expandedContext.name
+ ? // eslint-disable-next-line deprecation/deprecation
+ { ...finalContext.metadata, source: 'custom' }
+ : // eslint-disable-next-line deprecation/deprecation
+ finalContext.metadata;
+
+ latestRouteName = finalContext.name;
+ latestRouteSource = getSource(finalContext);
+
+ // eslint-disable-next-line deprecation/deprecation
+ if (finalContext.sampled === false) {
+ DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`);
+ }
+
+ DEBUG_BUILD && logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`);
+
+ const { location } = WINDOW;
+
+ const idleTransaction = startIdleTransaction(
+ hub,
+ finalContext,
+ idleTimeout,
+ finalTimeout,
+ true,
+ { location }, // for use in the tracesSampler
+ heartbeatInterval,
+ isPageloadTransaction, // should wait for finish signal if it's a pageload transaction
+ );
+
+ if (isPageloadTransaction) {
+ WINDOW.document.addEventListener('readystatechange', () => {
+ if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
+ idleTransaction.sendAutoFinishSignal();
+ }
+ });
+
+ if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
+ idleTransaction.sendAutoFinishSignal();
+ }
+ }
+
+ // eslint-disable-next-line deprecation/deprecation
+ const scope = hub.getScope();
+
+ // If it's a pageload and there is a meta tag set
+ // use the traceparentData as the propagation context
+ if (isPageloadTransaction && traceparentData) {
+ scope.setPropagationContext(propagationContext);
+ } else {
+ // Navigation transactions should set a new propagation context based on the
+ // created idle transaction.
+ scope.setPropagationContext({
+ traceId: idleTransaction.spanContext().traceId,
+ spanId: idleTransaction.spanContext().spanId,
+ parentSpanId: spanToJSON(idleTransaction).parent_span_id,
+ sampled: spanIsSampled(idleTransaction),
+ });
+ }
+
+ idleTransaction.registerBeforeFinishCallback(transaction => {
+ _collectWebVitals();
+ addPerformanceEntries(transaction);
+ });
+
+ return idleTransaction as Transaction;
+ }
+
+ return {
+ name: BROWSER_TRACING_INTEGRATION_ID,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ setupOnce: () => {},
+ afterAllSetup(client) {
+ const clientOptions = client.getOptions();
+
+ const { markBackgroundSpan, traceFetch, traceXHR, shouldCreateSpanForRequest, enableHTTPTimings, _experiments } =
+ options;
+
+ const clientOptionsTracePropagationTargets = clientOptions && clientOptions.tracePropagationTargets;
+ // There are three ways to configure tracePropagationTargets:
+ // 1. via top level client option `tracePropagationTargets`
+ // 2. via BrowserTracing option `tracePropagationTargets`
+ // 3. via BrowserTracing option `tracingOrigins` (deprecated)
+ //
+ // To avoid confusion, favour top level client option `tracePropagationTargets`, and fallback to
+ // BrowserTracing option `tracePropagationTargets` and then `tracingOrigins` (deprecated).
+ // This is done as it minimizes bundle size (we don't have to have undefined checks).
+ //
+ // If both 1 and either one of 2 or 3 are set (from above), we log out a warning.
+ // eslint-disable-next-line deprecation/deprecation
+ const tracePropagationTargets = clientOptionsTracePropagationTargets || options.tracePropagationTargets;
+ if (DEBUG_BUILD && _hasSetTracePropagationTargets && clientOptionsTracePropagationTargets) {
+ logger.warn(
+ '[Tracing] The `tracePropagationTargets` option was set in the BrowserTracing integration and top level `Sentry.init`. The top level `Sentry.init` value is being used.',
+ );
+ }
+
+ let activeSpan: Span | undefined;
+ let startingUrl: string | undefined = WINDOW.location.href;
+
+ if (client.on) {
+ client.on('startNavigationSpan', (context: StartSpanOptions) => {
+ // We check this inside of the hook handler, so that if a custom instrumentation triggers this,
+ // we don't need to check this option in the instrumentation, but can simply invoke it
+ // without needing to know the options of this integration
+ if (!options.instrumentNavigation) {
+ return;
+ }
+
+ if (activeSpan) {
+ DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`);
+ // If there's an open transaction on the scope, we need to finish it before creating an new one.
+ activeSpan.end();
+ }
+ activeSpan = _createRouteTransaction(context);
+ });
+
+ client.on('startPageLoadSpan', (context: StartSpanOptions) => {
+ // We check this inside of the hook handler, so that if a custom instrumentation triggers this,
+ // we don't need to check this option in the instrumentation, but can simply invoke it
+ // without needing to know the options of this integration
+ if (!options.instrumentPageLoad) {
+ return;
+ }
+
+ if (activeSpan) {
+ DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`);
+ // If there's an open transaction on the scope, we need to finish it before creating an new one.
+ activeSpan.end();
+ }
+ activeSpan = _createRouteTransaction(context);
+ });
+ }
+
+ if (options.instrumentPageLoad && client.emit && shouldUseDefaultPageLoadSpan) {
+ const context: StartSpanOptions = {
+ name: WINDOW.location.pathname,
+ // pageload should always start at timeOrigin (and needs to be in s, not ms)
+ startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
+ op: 'pageload',
+ origin: 'auto.pageload.browser',
+ metadata: { source: 'url' },
+ };
+ startBrowserTracingPageLoadSpan(context);
+ }
+
+ if (options.instrumentNavigation && client.emit) {
+ addHistoryInstrumentationHandler(({ to, from }) => {
+ /**
+ * This early return is there to account for some cases where a navigation transaction starts right after
+ * long-running pageload. We make sure that if `from` is undefined and a valid `startingURL` exists, we don't
+ * create an uneccessary navigation transaction.
+ *
+ * This was hard to duplicate, but this behavior stopped as soon as this fix was applied. This issue might also
+ * only be caused in certain development environments where the usage of a hot module reloader is causing
+ * errors.
+ */
+ if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) {
+ startingUrl = undefined;
+ return;
+ }
+
+ if (from !== to) {
+ startingUrl = undefined;
+ // We check this in here again, as a custom instrumentation may have been triggered in the meanwhile
+ if (shouldUseDefaultNavigationSpan) {
+ const context: StartSpanOptions = {
+ name: WINDOW.location.pathname,
+ op: 'navigation',
+ origin: 'auto.navigation.browser',
+ metadata: { source: 'url' },
+ };
+
+ startBrowserTracingNavigationSpan(context);
+ }
+ }
+ });
+ }
+
+ if (markBackgroundSpan) {
+ registerBackgroundTabDetection();
+ }
+
+ if (_experiments.enableInteractions) {
+ registerInteractionListener(options, latestRouteName, latestRouteSource);
+ }
+
+ instrumentOutgoingRequests({
+ traceFetch,
+ traceXHR,
+ tracePropagationTargets,
+ shouldCreateSpanForRequest,
+ enableHTTPTimings,
+ });
+ },
+ // TODO v8: Remove this again
+ // This is private API that we use to fix converted BrowserTracing integrations in Next.js & SvelteKit
+ options,
+ };
+}) satisfies IntegrationFn;
+
+export const browserTracingIntegration = defineIntegration(_browserTracingIntegration);
+
+/**
+ * Manually start a page load span.
+ * This will only do something if the BrowserTracing integration has been setup.
+ */
+export function startBrowserTracingPageLoadSpan(spanOptions: StartSpanOptions): void {
+ const client = getClient();
+ if (!client || !client.emit) {
+ return;
+ }
+
+ client.emit('startPageLoadSpan', spanOptions);
+ shouldUseDefaultPageLoadSpan = false;
+}
+
+/**
+ * Manually start a navigation span.
+ * This will only do something if the BrowserTracing integration has been setup.
+ */
+export function startBrowserTracingNavigationSpan(spanOptions: StartSpanOptions): void {
+ const client = getClient();
+ if (!client || !client.emit) {
+ return;
+ }
+
+ client.emit('startNavigationSpan', spanOptions);
+ shouldUseDefaultNavigationSpan = false;
+}
+
+/**
+ * Use this method if you want to disable the default navigation span.
+ * This is useful if you want to add custom routing instrumentation.
+ */
+export function disableDefaultBrowserTracingNavigationSpan(disable = true): void {
+ shouldUseDefaultNavigationSpan = !disable;
+}
+
+/**
+ * Use this method if you want to disable the default page load span.
+ * This is useful if you want to add custom routing instrumentation.
+ */
+export function disableDefaultBrowserTracingPageLoadSpan(disable = true): void {
+ shouldUseDefaultPageLoadSpan = !disable;
+}
+
+/** Returns the value of a meta tag */
+export function getMetaContent(metaName: string): string | undefined {
+ // Can't specify generic to `getDomElement` because tracing can be used
+ // in a variety of environments, have to disable `no-unsafe-member-access`
+ // as a result.
+ const metaTag = getDomElement(`meta[name=${metaName}]`);
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ return metaTag ? metaTag.getAttribute('content') : undefined;
+}
+
+/** Start listener for interaction transactions */
+function registerInteractionListener(
+ options: BrowserTracingOptions,
+ latestRouteName: string | undefined,
+ latestRouteSource: TransactionSource | undefined,
+): void {
+ let inflightInteractionTransaction: IdleTransaction | undefined;
+ const registerInteractionTransaction = (): void => {
+ const { idleTimeout, finalTimeout, heartbeatInterval } = options;
+ const op = 'ui.action.click';
+
+ // eslint-disable-next-line deprecation/deprecation
+ const currentTransaction = getActiveTransaction();
+ if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) {
+ DEBUG_BUILD &&
+ logger.warn(
+ `[Tracing] Did not create ${op} transaction because a pageload or navigation transaction is in progress.`,
+ );
+ return undefined;
+ }
+
+ if (inflightInteractionTransaction) {
+ inflightInteractionTransaction.setFinishReason('interactionInterrupted');
+ inflightInteractionTransaction.end();
+ inflightInteractionTransaction = undefined;
+ }
+
+ if (!latestRouteName) {
+ DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
+ return undefined;
+ }
+
+ const { location } = WINDOW;
+
+ const context: TransactionContext = {
+ name: latestRouteName,
+ op,
+ trimEnd: true,
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRouteSource || 'url',
+ },
+ };
+
+ inflightInteractionTransaction = startIdleTransaction(
+ // eslint-disable-next-line deprecation/deprecation
+ getCurrentHub(),
+ context,
+ idleTimeout,
+ finalTimeout,
+ true,
+ { location }, // for use in the tracesSampler
+ heartbeatInterval,
+ );
+ };
+
+ ['click'].forEach(type => {
+ addEventListener(type, registerInteractionTransaction, { once: false, capture: true });
+ });
+}
+
+function getSource(context: TransactionContext): TransactionSource | undefined {
+ const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
+ // eslint-disable-next-line deprecation/deprecation
+ const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
+ // eslint-disable-next-line deprecation/deprecation
+ const sourceFromMetadata = context.metadata && context.metadata.source;
+
+ return sourceFromAttributes || sourceFromData || sourceFromMetadata;
+}
diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts
index e9f61c73c0f3..2d8ffd9af135 100644
--- a/packages/tracing-internal/src/browser/browsertracing.ts
+++ b/packages/tracing-internal/src/browser/browsertracing.ts
@@ -344,13 +344,7 @@ export class BrowserTracing implements Integration {
finalContext.metadata;
this._latestRouteName = finalContext.name;
-
- // eslint-disable-next-line deprecation/deprecation
- const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
- // eslint-disable-next-line deprecation/deprecation
- const sourceFromMetadata = finalContext.metadata && finalContext.metadata.source;
-
- this._latestRouteSource = sourceFromData || sourceFromMetadata;
+ this._latestRouteSource = getSource(finalContext);
// eslint-disable-next-line deprecation/deprecation
if (finalContext.sampled === false) {
@@ -481,3 +475,13 @@ export function getMetaContent(metaName: string): string | undefined {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return metaTag ? metaTag.getAttribute('content') : undefined;
}
+
+function getSource(context: TransactionContext): TransactionSource | undefined {
+ const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
+ // eslint-disable-next-line deprecation/deprecation
+ const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
+ // eslint-disable-next-line deprecation/deprecation
+ const sourceFromMetadata = context.metadata && context.metadata.source;
+
+ return sourceFromAttributes || sourceFromData || sourceFromMetadata;
+}
diff --git a/packages/tracing-internal/src/browser/index.ts b/packages/tracing-internal/src/browser/index.ts
index 5b30bc519404..d9b0c347bb4e 100644
--- a/packages/tracing-internal/src/browser/index.ts
+++ b/packages/tracing-internal/src/browser/index.ts
@@ -3,6 +3,14 @@ export * from '../exports';
export type { RequestInstrumentationOptions } from './request';
export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browsertracing';
+export {
+ browserTracingIntegration,
+ startBrowserTracingNavigationSpan,
+ startBrowserTracingPageLoadSpan,
+ disableDefaultBrowserTracingNavigationSpan,
+ disableDefaultBrowserTracingPageLoadSpan,
+} from './browserTracingIntegration';
+
export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './request';
export {
diff --git a/packages/tracing-internal/src/index.ts b/packages/tracing-internal/src/index.ts
index 495d8dbb26b9..d3ace7e56b77 100644
--- a/packages/tracing-internal/src/index.ts
+++ b/packages/tracing-internal/src/index.ts
@@ -14,6 +14,11 @@ export type { LazyLoadedIntegration } from './node';
export {
BrowserTracing,
+ browserTracingIntegration,
+ startBrowserTracingNavigationSpan,
+ startBrowserTracingPageLoadSpan,
+ disableDefaultBrowserTracingNavigationSpan,
+ disableDefaultBrowserTracingPageLoadSpan,
BROWSER_TRACING_INTEGRATION_ID,
instrumentOutgoingRequests,
defaultRequestInstrumentationOptions,
diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts
index d8d09ec1431b..5db008b0ba37 100644
--- a/packages/types/src/client.ts
+++ b/packages/types/src/client.ts
@@ -15,6 +15,7 @@ import type { Scope } from './scope';
import type { SdkMetadata } from './sdkmetadata';
import type { Session, SessionAggregates } from './session';
import type { Severity, SeverityLevel } from './severity';
+import type { StartSpanOptions } from './startSpanOptions';
import type { Transaction } from './transaction';
import type { Transport, TransportMakeRequestResponse } from './transport';
@@ -272,6 +273,16 @@ export interface Client {
callback: (feedback: FeedbackEvent, options?: { includeReplay?: boolean }) => void,
): void;
+ /**
+ * A hook for BrowserTracing to trigger a span start for a page load.
+ */
+ on?(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void;
+
+ /**
+ * A hook for BrowserTracing to trigger a span for a navigation.
+ */
+ on?(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void;
+
/**
* Fire a hook event for transaction start.
* Expects to be given a transaction as the second argument.
@@ -333,5 +344,15 @@ export interface Client {
*/
emit?(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay?: boolean }): void;
+ /**
+ * Emit a hook event for BrowserTracing to trigger a span start for a page load.
+ */
+ emit?(hook: 'startPageLoadSpan', options: StartSpanOptions): void;
+
+ /**
+ * Emit a hook event for BrowserTracing to trigger a span for a navigation.
+ */
+ emit?(hook: 'startNavigationSpan', options: StartSpanOptions): void;
+
/* eslint-enable @typescript-eslint/unified-signatures */
}
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 7f9d66c904fa..5970383febc3 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -104,6 +104,7 @@ export type { StackFrame } from './stackframe';
export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace';
export type { TextEncoderInternal } from './textencoder';
export type { PropagationContext, TracePropagationTargets } from './tracing';
+export type { StartSpanOptions } from './startSpanOptions';
export type {
CustomSamplingContext,
SamplingContext,
diff --git a/packages/types/src/startSpanOptions.ts b/packages/types/src/startSpanOptions.ts
new file mode 100644
index 000000000000..bde20c2c87bf
--- /dev/null
+++ b/packages/types/src/startSpanOptions.ts
@@ -0,0 +1,108 @@
+import type { Instrumenter } from './instrumenter';
+import type { Primitive } from './misc';
+import type { Scope } from './scope';
+import type { SpanAttributes, SpanOrigin, SpanTimeInput } from './span';
+import type { TransactionContext, TransactionMetadata, TransactionSource } from './transaction';
+
+export interface StartSpanOptions extends TransactionContext {
+ /** A manually specified start time for the created `Span` object. */
+ startTime?: SpanTimeInput;
+
+ /** If defined, start this span off this scope instead off the current scope. */
+ scope?: Scope;
+
+ /** The name of the span. */
+ name: string;
+
+ /** An op for the span. This is a categorization for spans. */
+ op?: string;
+
+ /**
+ * The origin of the span - if it comes from auto instrumentation or manual instrumentation.
+ *
+ * @deprecated Set `attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]` instead.
+ */
+ origin?: SpanOrigin;
+
+ /** Attributes for the span. */
+ attributes?: SpanAttributes;
+
+ // All remaining fields are deprecated
+
+ /**
+ * @deprecated Manually set the end timestamp instead.
+ */
+ trimEnd?: boolean;
+
+ /**
+ * @deprecated This cannot be set manually anymore.
+ */
+ parentSampled?: boolean;
+
+ /**
+ * @deprecated Use attributes or set data on scopes instead.
+ */
+ metadata?: Partial;
+
+ /**
+ * The name thingy.
+ * @deprecated Use `name` instead.
+ */
+ description?: string;
+
+ /**
+ * @deprecated Use `span.setStatus()` instead.
+ */
+ status?: string;
+
+ /**
+ * @deprecated Use `scope` instead.
+ */
+ parentSpanId?: string;
+
+ /**
+ * @deprecated You cannot manually set the span to sampled anymore.
+ */
+ sampled?: boolean;
+
+ /**
+ * @deprecated You cannot manually set the spanId anymore.
+ */
+ spanId?: string;
+
+ /**
+ * @deprecated You cannot manually set the traceId anymore.
+ */
+ traceId?: string;
+
+ /**
+ * @deprecated Use an attribute instead.
+ */
+ source?: TransactionSource;
+
+ /**
+ * @deprecated Use attributes or set tags on the scope instead.
+ */
+ tags?: { [key: string]: Primitive };
+
+ /**
+ * @deprecated Use attributes instead.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ data?: { [key: string]: any };
+
+ /**
+ * @deprecated Use `startTime` instead.
+ */
+ startTimestamp?: number;
+
+ /**
+ * @deprecated Use `span.end()` instead.
+ */
+ endTimestamp?: number;
+
+ /**
+ * @deprecated You cannot set the instrumenter manually anymore.
+ */
+ instrumenter?: Instrumenter;
+}