From adccbe6e6c0075c90810e0435209420998f06358 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 26 Jan 2024 10:54:19 -0500 Subject: [PATCH] feat(tracing): Expose new `browserTracingIntegration` (#10351) Extracted this out of https://github.com/getsentry/sentry-javascript/pull/10327. This PR: * Introduces a new `browserTracingIntegration()` * Does NOT deprecate BrowserTracing yet, as custom implementations in Angular, Next, Sveltekit have not been migrated over yet, which would be weird. We can deprecate it once we moved these over. * Makes sure that custom implementations in Next & Sveltekit are "fixed" automatically * Uses a slim fork for the CDN bundles, to avoid shipping multiple implementations in there. * This means that in the CDN bundles, you can already use the new syntax, but you cannot pass a custom routing instrumentation anymore, and you also don't have the utility functions for it yet. I think this is the best tradeoff for now, and it's probably not a super common case to have custom routing instrumentation when using the Loader/CDN bundles (and if you do, you have to stick to `new BrowserTracing()` until v8). I copied the browser integration tests we have, which all pass! --- .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 | 21 +- 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 | 5 + 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 | 25 +- packages/sveltekit/src/client/sdk.ts | 21 +- packages/sveltekit/test/client/sdk.test.ts | 23 +- .../src/browser/browserTracingIntegration.ts | 549 ++++++++++++++++++ .../src/browser/browsertracing.ts | 18 +- .../tracing-internal/src/browser/index.ts | 8 + packages/tracing-internal/src/index.ts | 5 + packages/types/src/client.ts | 21 + packages/types/src/index.ts | 1 + packages/types/src/startSpanOptions.ts | 108 ++++ 81 files changed, 2008 insertions(+), 139 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..3960c25eccd3 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -1,5 +1,6 @@ import type { BrowserClient } from '@sentry/browser'; -import { getCurrentScope } from '@sentry/browser'; +import { getActiveSpan } from '@sentry/browser'; +import { browserTracingIntegration, getCurrentScope } from '@sentry/browser'; import * as SentryBrowser from '@sentry/browser'; import { BrowserTracing, SDK_VERSION, WINDOW, getClient } from '@sentry/browser'; import { vi } from 'vitest'; @@ -100,7 +101,7 @@ describe('Sentry client SDK', () => { delete globalThis.__SENTRY_TRACING__; }); - it('Overrides the automatically default BrowserTracing instance with a a user-provided instance', () => { + it('Overrides the automatically default BrowserTracing instance with a a user-provided BrowserTracing instance', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })], @@ -118,6 +119,22 @@ describe('Sentry client SDK', () => { // This shows that the user-configured options are still here expect(options.finalTimeout).toEqual(10); }); + + it('Overrides the automatically default BrowserTracing instance with a a user-provided browserTracingIntergation instance', () => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + browserTracingIntegration({ finalTimeout: 10, instrumentNavigation: false, instrumentPageLoad: false }), + ], + enableTracing: true, + }); + + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); + expect(browserTracing).toBeDefined(); + + // no active span means the settings were respected + expect(getActiveSpan()).toBeUndefined(); + }); }); }); }); diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts index 1bc58b780748..5fff014eaa8d 100644 --- a/packages/browser/src/helpers.ts +++ b/packages/browser/src/helpers.ts @@ -1,5 +1,7 @@ +import type { browserTracingIntegration } from '@sentry-internal/tracing'; +import { BrowserTracing } from '@sentry-internal/tracing'; import { captureException, withScope } from '@sentry/core'; -import type { DsnLike, Mechanism, WrappedFunction } from '@sentry/types'; +import type { DsnLike, Integration, Mechanism, WrappedFunction } from '@sentry/types'; import { GLOBAL_OBJ, addExceptionMechanism, @@ -185,3 +187,33 @@ export interface ReportDialogOptions { /** Callback after reportDialog closed */ onClose?(this: void): void; } + +/** + * This is a slim shim of `browserTracingIntegration` for the CDN bundles. + * Since the actual functional integration uses a different code from `BrowserTracing`, + * we want to avoid shipping both of them in the CDN bundles, as that would blow up the size. + * Instead, we provide a functional integration with the same API, but the old implementation. + * This means that it's not possible to register custom routing instrumentation, but that's OK for now. + * We also don't expose the utilities for this anyhow in the CDN bundles. + * For users that need custom routing in CDN bundles, they have to continue using `new BrowserTracing()` until v8. + */ +export function bundleBrowserTracingIntegration( + options: Parameters[0] = {}, +): Integration { + // Migrate some options from the old integration to the new one + const opts: ConstructorParameters[0] = options; + + if (typeof options.markBackgroundSpan === 'boolean') { + opts.markBackgroundTransactions = options.markBackgroundSpan; + } + + if (typeof options.instrumentPageLoad === 'boolean') { + opts.startTransactionOnPageLoad = options.instrumentPageLoad; + } + + if (typeof options.instrumentNavigation === 'boolean') { + opts.startTransactionOnLocationChange = options.instrumentNavigation; + } + + return new BrowserTracing(opts); +} diff --git a/packages/browser/src/index.bundle.feedback.ts b/packages/browser/src/index.bundle.feedback.ts index 5d3612106286..af4de5ea063d 100644 --- a/packages/browser/src/index.bundle.feedback.ts +++ b/packages/browser/src/index.bundle.feedback.ts @@ -1,6 +1,12 @@ // This is exported so the loader does not fail when switching off Replay/Tracing import { Feedback, feedbackIntegration } from '@sentry-internal/feedback'; -import { BrowserTracing, Replay, addTracingExtensions, replayIntegration } from '@sentry-internal/integration-shims'; +import { + BrowserTracing, + Replay, + addTracingExtensions, + browserTracingIntegration, + replayIntegration, +} from '@sentry-internal/integration-shims'; import * as Sentry from './index.bundle.base'; @@ -13,6 +19,7 @@ Sentry.Integrations.BrowserTracing = BrowserTracing; export * from './index.bundle.base'; export { BrowserTracing, + browserTracingIntegration, addTracingExtensions, // eslint-disable-next-line deprecation/deprecation Replay, diff --git a/packages/browser/src/index.bundle.replay.ts b/packages/browser/src/index.bundle.replay.ts index 2609e7d9b48c..175a435fadcf 100644 --- a/packages/browser/src/index.bundle.replay.ts +++ b/packages/browser/src/index.bundle.replay.ts @@ -3,6 +3,7 @@ import { BrowserTracing, Feedback, addTracingExtensions, + browserTracingIntegration, feedbackIntegration, } from '@sentry-internal/integration-shims'; import { Replay, replayIntegration } from '@sentry/replay'; @@ -18,6 +19,7 @@ Sentry.Integrations.BrowserTracing = BrowserTracing; export * from './index.bundle.base'; export { BrowserTracing, + browserTracingIntegration, addTracingExtensions, // eslint-disable-next-line deprecation/deprecation Replay, diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index e17c7de4159a..df151bba0a8f 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -1,6 +1,7 @@ import { Feedback, feedbackIntegration } from '@sentry-internal/feedback'; import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing'; import { Replay, replayIntegration } from '@sentry/replay'; +import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers'; import * as Sentry from './index.bundle.base'; @@ -23,6 +24,7 @@ export { feedbackIntegration, replayIntegration, BrowserTracing, + browserTracingIntegration, Span, addExtensionMethods, }; diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 5dc0537be064..2437a8546d5c 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -1,6 +1,7 @@ import { Feedback, feedbackIntegration } from '@sentry-internal/integration-shims'; import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing'; import { Replay, replayIntegration } from '@sentry/replay'; +import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers'; import * as Sentry from './index.bundle.base'; @@ -23,6 +24,7 @@ export { replayIntegration, feedbackIntegration, BrowserTracing, + browserTracingIntegration, Span, addExtensionMethods, }; diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index f810b61b92a7..2ca0613146f0 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -1,6 +1,7 @@ // This is exported so the loader does not fail when switching off Replay import { Feedback, Replay, feedbackIntegration, replayIntegration } from '@sentry-internal/integration-shims'; import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing'; +import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers'; import * as Sentry from './index.bundle.base'; @@ -23,6 +24,7 @@ export { feedbackIntegration, replayIntegration, BrowserTracing, + browserTracingIntegration, Span, addExtensionMethods, }; diff --git a/packages/browser/src/index.bundle.ts b/packages/browser/src/index.bundle.ts index a92ff6bf66ec..93a0b0cb498a 100644 --- a/packages/browser/src/index.bundle.ts +++ b/packages/browser/src/index.bundle.ts @@ -4,6 +4,7 @@ import { Feedback, Replay, addTracingExtensions, + browserTracingIntegration, feedbackIntegration, replayIntegration, } from '@sentry-internal/integration-shims'; @@ -24,6 +25,7 @@ export { Replay, // eslint-disable-next-line deprecation/deprecation Feedback, + browserTracingIntegration, feedbackIntegration, replayIntegration, }; diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 19c377fc5931..0c75bae6e1f9 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -57,6 +57,11 @@ export { BrowserTracing, defaultRequestInstrumentationOptions, instrumentOutgoingRequests, + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, + disableDefaultBrowserTracingNavigationSpan, + disableDefaultBrowserTracingPageLoadSpan, } from '@sentry-internal/tracing'; export type { RequestInstrumentationOptions } from '@sentry-internal/tracing'; export { diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index c7736e278c1c..a4d43fc58a8a 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -26,6 +26,7 @@ import type { SessionAggregates, Severity, SeverityLevel, + StartSpanOptions, Transaction, TransactionEvent, Transport, @@ -482,6 +483,12 @@ export abstract class BaseClient implements Client { callback: (feedback: FeedbackEvent, options?: { includeReplay: boolean }) => void, ): void; + /** @inheritdoc */ + public on(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void; + + /** @inheritdoc */ + public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; + /** @inheritdoc */ public on(hook: string, callback: unknown): void { if (!this._hooks[hook]) { @@ -522,6 +529,12 @@ export abstract class BaseClient implements Client { /** @inheritdoc */ public emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay: boolean }): void; + /** @inheritdoc */ + public emit(hook: 'startPageLoadSpan', options: StartSpanOptions): void; + + /** @inheritdoc */ + public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void; + /** @inheritdoc */ public emit(hook: string, ...rest: unknown[]): void { if (this._hooks[hook]) { diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index bb92373d3e58..885cbd7c9d08 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,15 +1,5 @@ -import type { - Instrumenter, - Primitive, - Scope, - Span, - SpanTimeInput, - TransactionContext, - TransactionMetadata, -} from '@sentry/types'; -import type { SpanAttributes } from '@sentry/types'; -import type { SpanOrigin } from '@sentry/types'; -import type { TransactionSource } from '@sentry/types'; +import type { Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types'; + import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -20,109 +10,6 @@ import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; import { spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; -interface StartSpanOptions extends Omit { - /** A manually specified start time for the created `Span` object. */ - startTime?: SpanTimeInput; - - /** If defined, start this span off this scope instead off the current scope. */ - scope?: Scope; - - /** The name of the span. */ - name: string; - - /** An op for the span. This is a categorization for spans. */ - op?: string; - - /** - * The origin of the span - if it comes from auto instrumentation or manual instrumentation. - * - * @deprecated Set `attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]` instead. - */ - origin?: SpanOrigin; - - /** Attributes for the span. */ - attributes?: SpanAttributes; - - // All remaining fields are deprecated - - /** - * @deprecated Manually set the end timestamp instead. - */ - trimEnd?: boolean; - - /** - * @deprecated This cannot be set manually anymore. - */ - parentSampled?: boolean; - - /** - * @deprecated Use attributes or set data on scopes instead. - */ - metadata?: Partial; - - /** - * The name thingy. - * @deprecated Use `name` instead. - */ - description?: string; - - /** - * @deprecated Use `span.setStatus()` instead. - */ - status?: string; - - /** - * @deprecated Use `scope` instead. - */ - parentSpanId?: string; - - /** - * @deprecated You cannot manually set the span to sampled anymore. - */ - sampled?: boolean; - - /** - * @deprecated You cannot manually set the spanId anymore. - */ - spanId?: string; - - /** - * @deprecated You cannot manually set the traceId anymore. - */ - traceId?: string; - - /** - * @deprecated Use an attribute instead. - */ - source?: TransactionSource; - - /** - * @deprecated Use attributes or set tags on the scope instead. - */ - tags?: { [key: string]: Primitive }; - - /** - * @deprecated Use attributes instead. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: { [key: string]: any }; - - /** - * @deprecated Use `startTime` instead. - */ - startTimestamp?: number; - - /** - * @deprecated Use `span.end()` instead. - */ - endTimestamp?: number; - - /** - * @deprecated You cannot set the instrumenter manually anymore. - */ - instrumenter?: Instrumenter; -} - /** * Wraps a function with a transaction/span and finishes the span after the function is done. * diff --git a/packages/integration-shims/src/BrowserTracing.ts b/packages/integration-shims/src/BrowserTracing.ts index 310dc589afe9..8e3d61bae58f 100644 --- a/packages/integration-shims/src/BrowserTracing.ts +++ b/packages/integration-shims/src/BrowserTracing.ts @@ -33,7 +33,16 @@ class BrowserTracingShim implements Integration { } } -export { BrowserTracingShim as BrowserTracing }; +/** + * This is a shim for the BrowserTracing integration. + * It is needed in order for the CDN bundles to continue working when users add/remove tracing + * from it, without changing their config. This is necessary for the loader mechanism. + */ +function browserTracingIntegrationShim(_options: unknown): Integration { + return new BrowserTracingShim({}); +} + +export { BrowserTracingShim as BrowserTracing, browserTracingIntegrationShim as browserTracingIntegration }; /** Shim function */ export function addTracingExtensions(): void { diff --git a/packages/integration-shims/src/index.ts b/packages/integration-shims/src/index.ts index 43243f69a194..bffdf82c99f7 100644 --- a/packages/integration-shims/src/index.ts +++ b/packages/integration-shims/src/index.ts @@ -3,9 +3,16 @@ export { Feedback, feedbackIntegration, } from './Feedback'; + export { // eslint-disable-next-line deprecation/deprecation Replay, replayIntegration, } from './Replay'; -export { BrowserTracing, addTracingExtensions } from './BrowserTracing'; + +export { + // eslint-disable-next-line deprecation/deprecation + BrowserTracing, + browserTracingIntegration, + addTracingExtensions, +} from './BrowserTracing'; diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index d1d5e1db7ff5..a1c20937f578 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -1,5 +1,5 @@ import { applySdkMetadata, hasTracingEnabled } from '@sentry/core'; -import type { BrowserOptions } from '@sentry/react'; +import type { BrowserOptions, browserTracingIntegration } from '@sentry/react'; import { Integrations as OriginalIntegrations, getCurrentScope, @@ -86,13 +86,30 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void { } } +function isNewBrowserTracingIntegration( + integration: Integration, +): integration is Integration & { options?: Parameters[0] } { + return !!integration.afterAllSetup && !!(integration as BrowserTracing).options; +} + function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] { const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing'); + + if (!browserTracing) { + return integrations; + } + + // If `browserTracingIntegration()` was added, we need to force-convert it to our custom one + if (isNewBrowserTracingIntegration(browserTracing)) { + const { options } = browserTracing; + integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options); + } + // If BrowserTracing was added, but it is not our forked version, // replace it with our forked version with the same options - if (browserTracing && !(browserTracing instanceof BrowserTracing)) { + if (!(browserTracing instanceof BrowserTracing)) { const options: ConstructorParameters[0] = (browserTracing as BrowserTracing).options; - // These two options are overwritten by the custom integration + // This option is overwritten by the custom integration delete options.routingInstrumentation; // eslint-disable-next-line deprecation/deprecation delete options.tracingOrigins; diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index 464b7db14dc7..f4ec99c3cc71 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -1,6 +1,7 @@ import { BaseClient } from '@sentry/core'; import * as SentryReact from '@sentry/react'; import type { BrowserClient } from '@sentry/react'; +import { browserTracingIntegration } from '@sentry/react'; import { WINDOW, getClient, getCurrentScope } from '@sentry/react'; import type { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -166,7 +167,7 @@ describe('Client init()', () => { init({ dsn: TEST_DSN, tracesSampleRate: 1.0, - integrations: [new BrowserTracing({ startTransactionOnLocationChange: false })], + integrations: [new BrowserTracing({ finalTimeout: 10 })], }); const client = getClient()!; @@ -177,7 +178,27 @@ describe('Client init()', () => { expect.objectContaining({ routingInstrumentation: nextRouterInstrumentation, // This proves it's still the user's copy - startTransactionOnLocationChange: false, + finalTimeout: 10, + }), + ); + }); + + it('forces correct router instrumentation if user provides `browserTracingIntegration`', () => { + init({ + dsn: TEST_DSN, + integrations: [browserTracingIntegration({ finalTimeout: 10 })], + enableTracing: true, + }); + + const client = getClient()!; + const integration = client.getIntegrationByName('BrowserTracing'); + + expect(integration).toBeDefined(); + expect(integration?.options).toEqual( + expect.objectContaining({ + routingInstrumentation: nextRouterInstrumentation, + // This proves it's still the user's copy + finalTimeout: 10, }), ); }); diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts index 7b9c608a862d..920b2db75193 100644 --- a/packages/sveltekit/src/client/sdk.ts +++ b/packages/sveltekit/src/client/sdk.ts @@ -1,5 +1,5 @@ import { applySdkMetadata, hasTracingEnabled } from '@sentry/core'; -import type { BrowserOptions } from '@sentry/svelte'; +import type { BrowserOptions, browserTracingIntegration } from '@sentry/svelte'; import { getDefaultIntegrations as getDefaultSvelteIntegrations } from '@sentry/svelte'; import { WINDOW, getCurrentScope, init as initSvelteSdk } from '@sentry/svelte'; import type { Integration } from '@sentry/types'; @@ -61,11 +61,28 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void { } } +function isNewBrowserTracingIntegration( + integration: Integration, +): integration is Integration & { options?: Parameters[0] } { + return !!integration.afterAllSetup && !!(integration as BrowserTracing).options; +} + function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] { const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing'); + + if (!browserTracing) { + return integrations; + } + + // If `browserTracingIntegration()` was added, we need to force-convert it to our custom one + if (isNewBrowserTracingIntegration(browserTracing)) { + const { options } = browserTracing; + integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options); + } + // If BrowserTracing was added, but it is not our forked version, // replace it with our forked version with the same options - if (browserTracing && !(browserTracing instanceof BrowserTracing)) { + if (!(browserTracing instanceof BrowserTracing)) { const options: ConstructorParameters[0] = (browserTracing as BrowserTracing).options; // This option is overwritten by the custom integration delete options.routingInstrumentation; diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts index 10292658bc54..4b0afb85bcd8 100644 --- a/packages/sveltekit/test/client/sdk.test.ts +++ b/packages/sveltekit/test/client/sdk.test.ts @@ -1,7 +1,7 @@ import { getClient, getCurrentScope } from '@sentry/core'; import type { BrowserClient } from '@sentry/svelte'; import * as SentrySvelte from '@sentry/svelte'; -import { SDK_VERSION, WINDOW } from '@sentry/svelte'; +import { SDK_VERSION, WINDOW, browserTracingIntegration } from '@sentry/svelte'; import { vi } from 'vitest'; import { BrowserTracing, init } from '../../src/client'; @@ -100,7 +100,26 @@ describe('Sentry client SDK', () => { it('Merges a user-provided BrowserTracing integration with the automatically added one', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })], + integrations: [new BrowserTracing({ finalTimeout: 10 })], + enableTracing: true, + }); + + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing') as BrowserTracing; + const options = browserTracing.options; + + expect(browserTracing).toBeDefined(); + + // This shows that the user-configured options are still here + expect(options.finalTimeout).toEqual(10); + + // But we force the routing instrumentation to be ours + expect(options.routingInstrumentation).toEqual(svelteKitRoutingInstrumentation); + }); + + it('Merges a user-provided browserTracingIntegration with the automatically added one', () => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [browserTracingIntegration({ finalTimeout: 10 })], enableTracing: true, }); diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts new file mode 100644 index 000000000000..8184ad058039 --- /dev/null +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -0,0 +1,549 @@ +/* eslint-disable max-lines, complexity */ +import type { IdleTransaction } from '@sentry/core'; +import { getClient } from '@sentry/core'; +import { defineIntegration, getCurrentHub } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + TRACING_DEFAULTS, + addTracingExtensions, + getActiveTransaction, + spanIsSampled, + spanToJSON, + startIdleTransaction, +} from '@sentry/core'; +import type { + IntegrationFn, + StartSpanOptions, + Transaction, + TransactionContext, + TransactionSource, +} from '@sentry/types'; +import type { Span } from '@sentry/types'; +import { + addHistoryInstrumentationHandler, + browserPerformanceTimeOrigin, + getDomElement, + logger, + tracingContextFromHeaders, +} from '@sentry/utils'; + +import { DEBUG_BUILD } from '../common/debug-build'; +import { registerBackgroundTabDetection } from './backgroundtab'; +import { + addPerformanceEntries, + startTrackingInteractions, + startTrackingLongTasks, + startTrackingWebVitals, +} from './metrics'; +import type { RequestInstrumentationOptions } from './request'; +import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request'; +import { WINDOW } from './types'; + +export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; + +/** Options for Browser Tracing integration */ +export interface BrowserTracingOptions extends RequestInstrumentationOptions { + /** + * The time to wait in ms until the transaction will be finished during an idle state. An idle state is defined + * by a moment where there are no in-progress spans. + * + * The transaction will use the end timestamp of the last finished span as the endtime for the transaction. + * If there are still active spans when this the `idleTimeout` is set, the `idleTimeout` will get reset. + * Time is in ms. + * + * Default: 1000 + */ + idleTimeout: number; + + /** + * The max duration for a transaction. If a transaction duration hits the `finalTimeout` value, it + * will be finished. + * Time is in ms. + * + * Default: 30000 + */ + finalTimeout: number; + + /** + * The heartbeat interval. If no new spans are started or open spans are finished within 3 heartbeats, + * the transaction will be finished. + * Time is in ms. + * + * Default: 5000 + */ + heartbeatInterval: number; + + /** + * If a span should be created on page load. + * Default: true + */ + instrumentPageLoad: boolean; + + /** + * If a span should be created on navigation (history change). + * Default: true + */ + instrumentNavigation: boolean; + + /** + * Flag spans where tabs moved to background with "cancelled". Browser background tab timing is + * not suited towards doing precise measurements of operations. By default, we recommend that this option + * be enabled as background transactions can mess up your statistics in nondeterministic ways. + * + * Default: true + */ + markBackgroundSpan: boolean; + + /** + * If true, Sentry will capture long tasks and add them to the corresponding transaction. + * + * Default: true + */ + enableLongTask: boolean; + + /** + * _metricOptions allows the user to send options to change how metrics are collected. + * + * _metricOptions is currently experimental. + * + * Default: undefined + */ + _metricOptions?: Partial<{ + /** + * @deprecated This property no longer has any effect and will be removed in v8. + */ + _reportAllChanges: boolean; + }>; + + /** + * _experiments allows the user to send options to define how this integration works. + * Note that the `enableLongTask` options is deprecated in favor of the option at the top level, and will be removed in v8. + * + * TODO (v8): Remove enableLongTask + * + * Default: undefined + */ + _experiments: Partial<{ + enableInteractions: boolean; + }>; + + /** + * A callback which is called before a span for a pageload or navigation is started. + * It receives the options passed to `startSpan`, and expects to return an updated options object. + */ + beforeStartSpan?: (options: StartSpanOptions) => StartSpanOptions; +} + +const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { + ...TRACING_DEFAULTS, + instrumentNavigation: true, + instrumentPageLoad: true, + markBackgroundSpan: true, + enableLongTask: true, + _experiments: {}, + ...defaultRequestInstrumentationOptions, +}; + +let shouldUseDefaultPageLoadSpan = true; +let shouldUseDefaultNavigationSpan = true; + +/** + * The Browser Tracing integration automatically instruments browser pageload/navigation + * actions as transactions, and captures requests, metrics and errors as spans. + * + * The integration can be configured with a variety of options, and can be extended to use + * any routing library. This integration uses {@see IdleTransaction} to create transactions. + */ +export const _browserTracingIntegration = ((_options: Partial = {}) => { + const _hasSetTracePropagationTargets = DEBUG_BUILD + ? !!( + // eslint-disable-next-line deprecation/deprecation + (_options.tracePropagationTargets || _options.tracingOrigins) + ) + : false; + + addTracingExtensions(); + + // TODO (v8): remove this block after tracingOrigins is removed + // Set tracePropagationTargets to tracingOrigins if specified by the user + // In case both are specified, tracePropagationTargets takes precedence + // eslint-disable-next-line deprecation/deprecation + if (!_options.tracePropagationTargets && _options.tracingOrigins) { + // eslint-disable-next-line deprecation/deprecation + _options.tracePropagationTargets = _options.tracingOrigins; + } + + const options = { + ...DEFAULT_BROWSER_TRACING_OPTIONS, + ..._options, + }; + + const _collectWebVitals = startTrackingWebVitals(); + + if (options.enableLongTask) { + startTrackingLongTasks(); + } + if (options._experiments.enableInteractions) { + startTrackingInteractions(); + } + + let latestRouteName: string | undefined; + let latestRouteSource: TransactionSource | undefined; + + /** Create routing idle transaction. */ + function _createRouteTransaction(context: TransactionContext): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation + const hub = getCurrentHub(); + + const { beforeStartSpan, idleTimeout, finalTimeout, heartbeatInterval } = options; + + const isPageloadTransaction = context.op === 'pageload'; + + const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : ''; + const baggage = isPageloadTransaction ? getMetaContent('baggage') : ''; + const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( + sentryTrace, + baggage, + ); + + const expandedContext: TransactionContext = { + ...context, + ...traceparentData, + metadata: { + // eslint-disable-next-line deprecation/deprecation + ...context.metadata, + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + }, + trimEnd: true, + }; + + const finalContext = beforeStartSpan ? beforeStartSpan(expandedContext) : expandedContext; + + // If `beforeStartSpan` set a custom name, record that fact + // eslint-disable-next-line deprecation/deprecation + finalContext.metadata = + finalContext.name !== expandedContext.name + ? // eslint-disable-next-line deprecation/deprecation + { ...finalContext.metadata, source: 'custom' } + : // eslint-disable-next-line deprecation/deprecation + finalContext.metadata; + + latestRouteName = finalContext.name; + latestRouteSource = getSource(finalContext); + + // eslint-disable-next-line deprecation/deprecation + if (finalContext.sampled === false) { + DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`); + } + + DEBUG_BUILD && logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`); + + const { location } = WINDOW; + + const idleTransaction = startIdleTransaction( + hub, + finalContext, + idleTimeout, + finalTimeout, + true, + { location }, // for use in the tracesSampler + heartbeatInterval, + isPageloadTransaction, // should wait for finish signal if it's a pageload transaction + ); + + if (isPageloadTransaction) { + WINDOW.document.addEventListener('readystatechange', () => { + if (['interactive', 'complete'].includes(WINDOW.document.readyState)) { + idleTransaction.sendAutoFinishSignal(); + } + }); + + if (['interactive', 'complete'].includes(WINDOW.document.readyState)) { + idleTransaction.sendAutoFinishSignal(); + } + } + + // eslint-disable-next-line deprecation/deprecation + const scope = hub.getScope(); + + // If it's a pageload and there is a meta tag set + // use the traceparentData as the propagation context + if (isPageloadTransaction && traceparentData) { + scope.setPropagationContext(propagationContext); + } else { + // Navigation transactions should set a new propagation context based on the + // created idle transaction. + scope.setPropagationContext({ + traceId: idleTransaction.spanContext().traceId, + spanId: idleTransaction.spanContext().spanId, + parentSpanId: spanToJSON(idleTransaction).parent_span_id, + sampled: spanIsSampled(idleTransaction), + }); + } + + idleTransaction.registerBeforeFinishCallback(transaction => { + _collectWebVitals(); + addPerformanceEntries(transaction); + }); + + return idleTransaction as Transaction; + } + + return { + name: BROWSER_TRACING_INTEGRATION_ID, + // eslint-disable-next-line @typescript-eslint/no-empty-function + setupOnce: () => {}, + afterAllSetup(client) { + const clientOptions = client.getOptions(); + + const { markBackgroundSpan, traceFetch, traceXHR, shouldCreateSpanForRequest, enableHTTPTimings, _experiments } = + options; + + const clientOptionsTracePropagationTargets = clientOptions && clientOptions.tracePropagationTargets; + // There are three ways to configure tracePropagationTargets: + // 1. via top level client option `tracePropagationTargets` + // 2. via BrowserTracing option `tracePropagationTargets` + // 3. via BrowserTracing option `tracingOrigins` (deprecated) + // + // To avoid confusion, favour top level client option `tracePropagationTargets`, and fallback to + // BrowserTracing option `tracePropagationTargets` and then `tracingOrigins` (deprecated). + // This is done as it minimizes bundle size (we don't have to have undefined checks). + // + // If both 1 and either one of 2 or 3 are set (from above), we log out a warning. + // eslint-disable-next-line deprecation/deprecation + const tracePropagationTargets = clientOptionsTracePropagationTargets || options.tracePropagationTargets; + if (DEBUG_BUILD && _hasSetTracePropagationTargets && clientOptionsTracePropagationTargets) { + logger.warn( + '[Tracing] The `tracePropagationTargets` option was set in the BrowserTracing integration and top level `Sentry.init`. The top level `Sentry.init` value is being used.', + ); + } + + let activeSpan: Span | undefined; + let startingUrl: string | undefined = WINDOW.location.href; + + if (client.on) { + client.on('startNavigationSpan', (context: StartSpanOptions) => { + // We check this inside of the hook handler, so that if a custom instrumentation triggers this, + // we don't need to check this option in the instrumentation, but can simply invoke it + // without needing to know the options of this integration + if (!options.instrumentNavigation) { + return; + } + + if (activeSpan) { + DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`); + // If there's an open transaction on the scope, we need to finish it before creating an new one. + activeSpan.end(); + } + activeSpan = _createRouteTransaction(context); + }); + + client.on('startPageLoadSpan', (context: StartSpanOptions) => { + // We check this inside of the hook handler, so that if a custom instrumentation triggers this, + // we don't need to check this option in the instrumentation, but can simply invoke it + // without needing to know the options of this integration + if (!options.instrumentPageLoad) { + return; + } + + if (activeSpan) { + DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`); + // If there's an open transaction on the scope, we need to finish it before creating an new one. + activeSpan.end(); + } + activeSpan = _createRouteTransaction(context); + }); + } + + if (options.instrumentPageLoad && client.emit && shouldUseDefaultPageLoadSpan) { + const context: StartSpanOptions = { + name: WINDOW.location.pathname, + // pageload should always start at timeOrigin (and needs to be in s, not ms) + startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined, + op: 'pageload', + origin: 'auto.pageload.browser', + metadata: { source: 'url' }, + }; + startBrowserTracingPageLoadSpan(context); + } + + if (options.instrumentNavigation && client.emit) { + addHistoryInstrumentationHandler(({ to, from }) => { + /** + * This early return is there to account for some cases where a navigation transaction starts right after + * long-running pageload. We make sure that if `from` is undefined and a valid `startingURL` exists, we don't + * create an uneccessary navigation transaction. + * + * This was hard to duplicate, but this behavior stopped as soon as this fix was applied. This issue might also + * only be caused in certain development environments where the usage of a hot module reloader is causing + * errors. + */ + if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) { + startingUrl = undefined; + return; + } + + if (from !== to) { + startingUrl = undefined; + // We check this in here again, as a custom instrumentation may have been triggered in the meanwhile + if (shouldUseDefaultNavigationSpan) { + const context: StartSpanOptions = { + name: WINDOW.location.pathname, + op: 'navigation', + origin: 'auto.navigation.browser', + metadata: { source: 'url' }, + }; + + startBrowserTracingNavigationSpan(context); + } + } + }); + } + + if (markBackgroundSpan) { + registerBackgroundTabDetection(); + } + + if (_experiments.enableInteractions) { + registerInteractionListener(options, latestRouteName, latestRouteSource); + } + + instrumentOutgoingRequests({ + traceFetch, + traceXHR, + tracePropagationTargets, + shouldCreateSpanForRequest, + enableHTTPTimings, + }); + }, + // TODO v8: Remove this again + // This is private API that we use to fix converted BrowserTracing integrations in Next.js & SvelteKit + options, + }; +}) satisfies IntegrationFn; + +export const browserTracingIntegration = defineIntegration(_browserTracingIntegration); + +/** + * Manually start a page load span. + * This will only do something if the BrowserTracing integration has been setup. + */ +export function startBrowserTracingPageLoadSpan(spanOptions: StartSpanOptions): void { + const client = getClient(); + if (!client || !client.emit) { + return; + } + + client.emit('startPageLoadSpan', spanOptions); + shouldUseDefaultPageLoadSpan = false; +} + +/** + * Manually start a navigation span. + * This will only do something if the BrowserTracing integration has been setup. + */ +export function startBrowserTracingNavigationSpan(spanOptions: StartSpanOptions): void { + const client = getClient(); + if (!client || !client.emit) { + return; + } + + client.emit('startNavigationSpan', spanOptions); + shouldUseDefaultNavigationSpan = false; +} + +/** + * Use this method if you want to disable the default navigation span. + * This is useful if you want to add custom routing instrumentation. + */ +export function disableDefaultBrowserTracingNavigationSpan(disable = true): void { + shouldUseDefaultNavigationSpan = !disable; +} + +/** + * Use this method if you want to disable the default page load span. + * This is useful if you want to add custom routing instrumentation. + */ +export function disableDefaultBrowserTracingPageLoadSpan(disable = true): void { + shouldUseDefaultPageLoadSpan = !disable; +} + +/** Returns the value of a meta tag */ +export function getMetaContent(metaName: string): string | undefined { + // Can't specify generic to `getDomElement` because tracing can be used + // in a variety of environments, have to disable `no-unsafe-member-access` + // as a result. + const metaTag = getDomElement(`meta[name=${metaName}]`); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return metaTag ? metaTag.getAttribute('content') : undefined; +} + +/** Start listener for interaction transactions */ +function registerInteractionListener( + options: BrowserTracingOptions, + latestRouteName: string | undefined, + latestRouteSource: TransactionSource | undefined, +): void { + let inflightInteractionTransaction: IdleTransaction | undefined; + const registerInteractionTransaction = (): void => { + const { idleTimeout, finalTimeout, heartbeatInterval } = options; + const op = 'ui.action.click'; + + // eslint-disable-next-line deprecation/deprecation + const currentTransaction = getActiveTransaction(); + if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) { + DEBUG_BUILD && + logger.warn( + `[Tracing] Did not create ${op} transaction because a pageload or navigation transaction is in progress.`, + ); + return undefined; + } + + if (inflightInteractionTransaction) { + inflightInteractionTransaction.setFinishReason('interactionInterrupted'); + inflightInteractionTransaction.end(); + inflightInteractionTransaction = undefined; + } + + if (!latestRouteName) { + DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`); + return undefined; + } + + const { location } = WINDOW; + + const context: TransactionContext = { + name: latestRouteName, + op, + trimEnd: true, + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRouteSource || 'url', + }, + }; + + inflightInteractionTransaction = startIdleTransaction( + // eslint-disable-next-line deprecation/deprecation + getCurrentHub(), + context, + idleTimeout, + finalTimeout, + true, + { location }, // for use in the tracesSampler + heartbeatInterval, + ); + }; + + ['click'].forEach(type => { + addEventListener(type, registerInteractionTransaction, { once: false, capture: true }); + }); +} + +function getSource(context: TransactionContext): TransactionSource | undefined { + const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromMetadata = context.metadata && context.metadata.source; + + return sourceFromAttributes || sourceFromData || sourceFromMetadata; +} diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index e9f61c73c0f3..2d8ffd9af135 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -344,13 +344,7 @@ export class BrowserTracing implements Integration { finalContext.metadata; this._latestRouteName = finalContext.name; - - // eslint-disable-next-line deprecation/deprecation - const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; - // eslint-disable-next-line deprecation/deprecation - const sourceFromMetadata = finalContext.metadata && finalContext.metadata.source; - - this._latestRouteSource = sourceFromData || sourceFromMetadata; + this._latestRouteSource = getSource(finalContext); // eslint-disable-next-line deprecation/deprecation if (finalContext.sampled === false) { @@ -481,3 +475,13 @@ export function getMetaContent(metaName: string): string | undefined { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return metaTag ? metaTag.getAttribute('content') : undefined; } + +function getSource(context: TransactionContext): TransactionSource | undefined { + const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromMetadata = context.metadata && context.metadata.source; + + return sourceFromAttributes || sourceFromData || sourceFromMetadata; +} diff --git a/packages/tracing-internal/src/browser/index.ts b/packages/tracing-internal/src/browser/index.ts index 5b30bc519404..d9b0c347bb4e 100644 --- a/packages/tracing-internal/src/browser/index.ts +++ b/packages/tracing-internal/src/browser/index.ts @@ -3,6 +3,14 @@ export * from '../exports'; export type { RequestInstrumentationOptions } from './request'; export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browsertracing'; +export { + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, + disableDefaultBrowserTracingNavigationSpan, + disableDefaultBrowserTracingPageLoadSpan, +} from './browserTracingIntegration'; + export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './request'; export { diff --git a/packages/tracing-internal/src/index.ts b/packages/tracing-internal/src/index.ts index 495d8dbb26b9..d3ace7e56b77 100644 --- a/packages/tracing-internal/src/index.ts +++ b/packages/tracing-internal/src/index.ts @@ -14,6 +14,11 @@ export type { LazyLoadedIntegration } from './node'; export { BrowserTracing, + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, + disableDefaultBrowserTracingNavigationSpan, + disableDefaultBrowserTracingPageLoadSpan, BROWSER_TRACING_INTEGRATION_ID, instrumentOutgoingRequests, defaultRequestInstrumentationOptions, diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index d8d09ec1431b..5db008b0ba37 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -15,6 +15,7 @@ import type { Scope } from './scope'; import type { SdkMetadata } from './sdkmetadata'; import type { Session, SessionAggregates } from './session'; import type { Severity, SeverityLevel } from './severity'; +import type { StartSpanOptions } from './startSpanOptions'; import type { Transaction } from './transaction'; import type { Transport, TransportMakeRequestResponse } from './transport'; @@ -272,6 +273,16 @@ export interface Client { callback: (feedback: FeedbackEvent, options?: { includeReplay?: boolean }) => void, ): void; + /** + * A hook for BrowserTracing to trigger a span start for a page load. + */ + on?(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void; + + /** + * A hook for BrowserTracing to trigger a span for a navigation. + */ + on?(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; + /** * Fire a hook event for transaction start. * Expects to be given a transaction as the second argument. @@ -333,5 +344,15 @@ export interface Client { */ emit?(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay?: boolean }): void; + /** + * Emit a hook event for BrowserTracing to trigger a span start for a page load. + */ + emit?(hook: 'startPageLoadSpan', options: StartSpanOptions): void; + + /** + * Emit a hook event for BrowserTracing to trigger a span for a navigation. + */ + emit?(hook: 'startNavigationSpan', options: StartSpanOptions): void; + /* eslint-enable @typescript-eslint/unified-signatures */ } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 7f9d66c904fa..5970383febc3 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -104,6 +104,7 @@ export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; export type { TextEncoderInternal } from './textencoder'; export type { PropagationContext, TracePropagationTargets } from './tracing'; +export type { StartSpanOptions } from './startSpanOptions'; export type { CustomSamplingContext, SamplingContext, diff --git a/packages/types/src/startSpanOptions.ts b/packages/types/src/startSpanOptions.ts new file mode 100644 index 000000000000..bde20c2c87bf --- /dev/null +++ b/packages/types/src/startSpanOptions.ts @@ -0,0 +1,108 @@ +import type { Instrumenter } from './instrumenter'; +import type { Primitive } from './misc'; +import type { Scope } from './scope'; +import type { SpanAttributes, SpanOrigin, SpanTimeInput } from './span'; +import type { TransactionContext, TransactionMetadata, TransactionSource } from './transaction'; + +export interface StartSpanOptions extends TransactionContext { + /** A manually specified start time for the created `Span` object. */ + startTime?: SpanTimeInput; + + /** If defined, start this span off this scope instead off the current scope. */ + scope?: Scope; + + /** The name of the span. */ + name: string; + + /** An op for the span. This is a categorization for spans. */ + op?: string; + + /** + * The origin of the span - if it comes from auto instrumentation or manual instrumentation. + * + * @deprecated Set `attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]` instead. + */ + origin?: SpanOrigin; + + /** Attributes for the span. */ + attributes?: SpanAttributes; + + // All remaining fields are deprecated + + /** + * @deprecated Manually set the end timestamp instead. + */ + trimEnd?: boolean; + + /** + * @deprecated This cannot be set manually anymore. + */ + parentSampled?: boolean; + + /** + * @deprecated Use attributes or set data on scopes instead. + */ + metadata?: Partial; + + /** + * The name thingy. + * @deprecated Use `name` instead. + */ + description?: string; + + /** + * @deprecated Use `span.setStatus()` instead. + */ + status?: string; + + /** + * @deprecated Use `scope` instead. + */ + parentSpanId?: string; + + /** + * @deprecated You cannot manually set the span to sampled anymore. + */ + sampled?: boolean; + + /** + * @deprecated You cannot manually set the spanId anymore. + */ + spanId?: string; + + /** + * @deprecated You cannot manually set the traceId anymore. + */ + traceId?: string; + + /** + * @deprecated Use an attribute instead. + */ + source?: TransactionSource; + + /** + * @deprecated Use attributes or set tags on the scope instead. + */ + tags?: { [key: string]: Primitive }; + + /** + * @deprecated Use attributes instead. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: { [key: string]: any }; + + /** + * @deprecated Use `startTime` instead. + */ + startTimestamp?: number; + + /** + * @deprecated Use `span.end()` instead. + */ + endTimestamp?: number; + + /** + * @deprecated You cannot set the instrumenter manually anymore. + */ + instrumenter?: Instrumenter; +}