From c1cae1a16456613dc3efd34751512fa4755efb8e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 25 Jan 2024 13:33:41 -0500 Subject: [PATCH 1/5] feat(tracing): Expose new `browserTracingIntegration` --- .size-limit.js | 7 + .../replay/replayIntegrationShim /init.js | 23 + .../replayIntegrationShim /template.html | 9 + .../replay/replayIntegrationShim /test.ts | 35 ++ .../backgroundtab-custom/init.js | 9 + .../backgroundtab-custom/subject.js | 11 + .../backgroundtab-custom/template.html | 10 + .../backgroundtab-custom/test.ts | 45 ++ .../backgroundtab-pageload/subject.js | 8 + .../backgroundtab-pageload/template.html | 9 + .../backgroundtab-pageload/test.ts | 23 + .../http-timings/init.js | 16 + .../http-timings/subject.js | 1 + .../http-timings/test.ts | 58 ++ .../tracing/browserTracingIntegration/init.js | 9 + .../interactions/assets/script.js | 17 + .../interactions/init.js | 17 + .../interactions/template.html | 12 + .../interactions/test.ts | 114 ++++ .../long-tasks-disabled/assets/script.js | 12 + .../long-tasks-disabled/init.js | 9 + .../long-tasks-disabled/template.html | 10 + .../long-tasks-disabled/test.ts | 23 + .../long-tasks-enabled/assets/script.js | 12 + .../long-tasks-enabled/init.js | 13 + .../long-tasks-enabled/template.html | 10 + .../long-tasks-enabled/test.ts | 38 ++ .../browserTracingIntegration/meta/init.js | 10 + .../meta/template.html | 11 + .../browserTracingIntegration/meta/test.ts | 96 ++++ .../navigation/test.ts | 51 ++ .../pageload/init.js | 10 + .../pageload/test.ts | 24 + .../pageloadDelayed/init.js | 13 + .../pageloadDelayed/test.ts | 26 + .../pageloadWithHeartbeatTimeout/init.js | 14 + .../pageloadWithHeartbeatTimeout/test.ts | 27 + .../customTargets/init.js | 9 + .../customTargets/subject.js | 1 + .../customTargets/test.ts | 33 ++ .../customTargetsAndOrigins/init.js | 11 + .../customTargetsAndOrigins/subject.js | 1 + .../customTargetsAndOrigins/test.ts | 32 ++ .../customTracingOrigins/init.js | 9 + .../customTracingOrigins/subject.js | 1 + .../customTracingOrigins/test.ts | 32 ++ .../defaultTargetsMatch/init.js | 9 + .../defaultTargetsMatch/subject.js | 1 + .../defaultTargetsMatch/test.ts | 32 ++ .../defaultTargetsNoMatch/init.js | 9 + .../defaultTargetsNoMatch/subject.js | 1 + .../defaultTargetsNoMatch/test.ts | 32 ++ .../browserTracingIntegrationHashShim/init.js | 12 + .../template.html | 9 + .../browserTracingIntegrationHashShim/test.ts | 36 ++ .../browserTracingIntegrationShim/init.js | 2 +- .../browserTracingIntegrationShim/test.ts | 2 +- packages/astro/test/client/sdk.test.ts | 23 +- packages/browser/src/helpers.ts | 34 +- packages/browser/src/index.bundle.feedback.ts | 9 +- packages/browser/src/index.bundle.replay.ts | 2 + .../index.bundle.tracing.replay.feedback.ts | 2 + .../src/index.bundle.tracing.replay.ts | 2 + packages/browser/src/index.bundle.tracing.ts | 2 + packages/browser/src/index.bundle.ts | 2 + packages/browser/src/index.ts | 3 + packages/core/src/baseclient.ts | 13 + packages/core/src/tracing/trace.ts | 117 +--- .../integration-shims/src/BrowserTracing.ts | 11 +- packages/integration-shims/src/index.ts | 9 +- packages/nextjs/src/client/index.ts | 23 +- packages/nextjs/test/clientSdk.test.ts | 27 +- packages/sveltekit/src/client/sdk.ts | 21 +- packages/sveltekit/test/client/sdk.test.ts | 23 +- .../src/browser/browserTracingIntegration.ts | 522 ++++++++++++++++++ .../tracing-internal/src/browser/index.ts | 6 + packages/tracing-internal/src/index.ts | 3 + packages/types/src/client.ts | 21 + packages/types/src/index.ts | 1 + packages/types/src/startSpanOptions.ts | 108 ++++ 80 files changed, 1966 insertions(+), 134 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js create mode 100644 dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html create mode 100644 dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts create mode 100644 packages/tracing-internal/src/browser/browserTracingIntegration.ts create mode 100644 packages/types/src/startSpanOptions.ts 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..8ba54729c8a1 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 type { BrowserClient} 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..a3df9a6cc44a 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -57,6 +57,9 @@ export { BrowserTracing, defaultRequestInstrumentationOptions, instrumentOutgoingRequests, + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, } 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..f73b3228eb5b 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 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..3c4e218f728f --- /dev/null +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -0,0 +1,522 @@ +/* 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; + + // 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; + + latestRouteSource = sourceFromData || sourceFromMetadata; + + // 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) => { + 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) => { + 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; + 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; +} + +/** 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 }); + }); +} diff --git a/packages/tracing-internal/src/browser/index.ts b/packages/tracing-internal/src/browser/index.ts index 5b30bc519404..7d29d2f11e9e 100644 --- a/packages/tracing-internal/src/browser/index.ts +++ b/packages/tracing-internal/src/browser/index.ts @@ -3,6 +3,12 @@ export * from '../exports'; export type { RequestInstrumentationOptions } from './request'; export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browsertracing'; +export { + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, +} 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..233baae9c308 100644 --- a/packages/tracing-internal/src/index.ts +++ b/packages/tracing-internal/src/index.ts @@ -14,6 +14,9 @@ export type { LazyLoadedIntegration } from './node'; export { BrowserTracing, + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, 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; +} From a0a79faafd524bcb5fb15f135a11eef90366025e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 25 Jan 2024 14:38:32 -0500 Subject: [PATCH 2/5] better comments --- .../src/browser/browserTracingIntegration.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 3c4e218f728f..11e4ea9803e3 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -329,6 +329,9 @@ export const _browserTracingIntegration = ((_options: Partial { + // 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; } @@ -342,6 +345,9 @@ export const _browserTracingIntegration = ((_options: Partial { + // 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; } @@ -385,6 +391,7 @@ export const _browserTracingIntegration = ((_options: Partial Date: Thu, 25 Jan 2024 15:12:16 -0500 Subject: [PATCH 3/5] fix linting --- packages/astro/test/client/sdk.test.ts | 2 +- packages/nextjs/test/clientSdk.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index 8ba54729c8a1..3960c25eccd3 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -1,4 +1,4 @@ -import type { BrowserClient} from '@sentry/browser'; +import type { BrowserClient } from '@sentry/browser'; import { getActiveSpan } from '@sentry/browser'; import { browserTracingIntegration, getCurrentScope } from '@sentry/browser'; import * as SentryBrowser from '@sentry/browser'; diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index f73b3228eb5b..f4ec99c3cc71 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -1,6 +1,6 @@ import { BaseClient } from '@sentry/core'; import * as SentryReact from '@sentry/react'; -import type { BrowserClient} 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'; From af7fc5cdbf76512711014717609441f251c21db3 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 25 Jan 2024 16:47:42 -0500 Subject: [PATCH 4/5] add method to disable defaults --- packages/browser/src/index.ts | 2 ++ .../src/browser/browserTracingIntegration.ts | 16 ++++++++++++++++ packages/tracing-internal/src/browser/index.ts | 2 ++ packages/tracing-internal/src/index.ts | 2 ++ 4 files changed, 22 insertions(+) diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index a3df9a6cc44a..0c75bae6e1f9 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -60,6 +60,8 @@ export { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, + disableDefaultBrowserTracingNavigationSpan, + disableDefaultBrowserTracingPageLoadSpan, } from '@sentry-internal/tracing'; export type { RequestInstrumentationOptions } from '@sentry-internal/tracing'; export { diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 11e4ea9803e3..1f163c8d5708 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -458,6 +458,22 @@ export function startBrowserTracingNavigationSpan(spanOptions: StartSpanOptions) 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 diff --git a/packages/tracing-internal/src/browser/index.ts b/packages/tracing-internal/src/browser/index.ts index 7d29d2f11e9e..d9b0c347bb4e 100644 --- a/packages/tracing-internal/src/browser/index.ts +++ b/packages/tracing-internal/src/browser/index.ts @@ -7,6 +7,8 @@ export { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, + disableDefaultBrowserTracingNavigationSpan, + disableDefaultBrowserTracingPageLoadSpan, } from './browserTracingIntegration'; export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './request'; diff --git a/packages/tracing-internal/src/index.ts b/packages/tracing-internal/src/index.ts index 233baae9c308..d3ace7e56b77 100644 --- a/packages/tracing-internal/src/index.ts +++ b/packages/tracing-internal/src/index.ts @@ -17,6 +17,8 @@ export { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, + disableDefaultBrowserTracingNavigationSpan, + disableDefaultBrowserTracingPageLoadSpan, BROWSER_TRACING_INTEGRATION_ID, instrumentOutgoingRequests, defaultRequestInstrumentationOptions, From 2f128612cc01dff1db6e9bdaba2e3a64ab63efac Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 26 Jan 2024 09:05:21 -0500 Subject: [PATCH 5/5] fix source handling --- .../src/browser/browserTracingIntegration.ts | 18 +++++++++++------- .../src/browser/browsertracing.ts | 18 +++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 1f163c8d5708..8184ad058039 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -229,13 +229,7 @@ export const _browserTracingIntegration = ((_options: Partial