diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml index 99deb0e2677c..3becba39719e 100644 --- a/.github/dependency-review-config.yml +++ b/.github/dependency-review-config.yml @@ -1,7 +1,9 @@ fail-on-severity: 'high' allow-ghsas: # dependency review does not allow specific file exclusions - # we use an older version of NextJS in our tests and thus need to + # we use an older version of NextJS in our tests and thus need to # exclude this # once our minimum supported version is over 14.1.1 this can be removed - GHSA-fr5h-rqp8-mj6g + # we need this for an E2E test for the minimum required version of Nuxt 3.7.0 + - GHSA-v784-fjjh-f8r4 diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index b964e6b3d1b0..046ede6e84d6 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -14,11 +14,11 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/packages/*/*.tgz - ${{ github.workspace }}/dev-packages/test-utils/build ${{ github.workspace }}/node_modules ${{ github.workspace }}/packages/*/node_modules ${{ github.workspace }}/dev-packages/*/node_modules - ${{ github.workspace }}/packages/utils/build + ${{ github.workspace }}/dev-packages/*/build + ${{ github.workspace }}/packages/*/build permissions: contents: read diff --git a/.size-limit.js b/.size-limit.js index b28892aecb89..dc2b7af8128f 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -40,7 +40,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '36.5 KB', + limit: '37.5 KB', }, { name: '@sentry/browser (incl. Tracing, Replay)', @@ -124,7 +124,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '39.5 KB', + limit: '40.5 KB', }, // Vue SDK (ESM) { @@ -132,14 +132,14 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init'), gzip: true, - limit: '28 KB', + limit: '29 KB', }, { name: '@sentry/vue (incl. Tracing)', path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '38.5 KB', + limit: '39.5 KB', }, // Svelte SDK (ESM) { @@ -187,7 +187,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.min.js'), gzip: false, brotli: false, - limit: '113 KB', + limit: '120 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed', @@ -219,7 +219,7 @@ module.exports = [ import: createImport('init'), ignore: ['$app/stores'], gzip: true, - limit: '37 KB', + limit: '38 KB', }, // Node SDK (ESM) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0db74fffcf..09f035ce0315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,100 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.41.0 + +### Important Changes + +- **meta(nuxt): Require minimum Nuxt v3.7.0 ([#14473](https://github.com/getsentry/sentry-javascript/pull/14473))** + + We formalized that the Nuxt SDK is at minimum compatible with Nuxt version 3.7.0 and above. + Additionally, the SDK requires the implicit `nitropack` dependency to satisfy version `^2.6.1` and `ofetch` to satisfy `^1.3.3`. + It is recommended to check your lock-files and manually upgrade these dependencies if they don't match the version ranges. + +### Deprecations + +We are deprecating a few APIs which will be removed in the next major. + +The following deprecations will _potentially_ affect you: + +- **feat(core): Update & deprecate `undefined` option handling ([#14450](https://github.com/getsentry/sentry-javascript/pull/14450))** + + In the next major version we will change how passing `undefined` to `tracesSampleRate` / `tracesSampler` / `enableTracing` will behave. + + Currently, doing the following: + + ```ts + Sentry.init({ + tracesSampleRate: undefined, + }); + ``` + + Will result in tracing being _enabled_ (although no spans will be generated) because the `tracesSampleRate` key is present in the options object. + In the next major version, this behavior will be changed so that passing `undefined` (or rather having a `tracesSampleRate` key) will result in tracing being disabled, the same as not passing the option at all. + If you are currently relying on `undefined` being passed, and and thus have tracing enabled, it is recommended to update your config to set e.g. `tracesSampleRate: 0` instead, which will also enable tracing in v9. + + The same applies to `tracesSampler` and `enableTracing`. + +- **feat(core): Log warnings when returning `null` in `beforeSendSpan` ([#14433](https://github.com/getsentry/sentry-javascript/pull/14433))** + + Currently, the `beforeSendSpan` option in `Sentry.init()` allows you to drop individual spans from a trace by returning `null` from the hook. + Since this API lends itself to creating "gaps" inside traces, we decided to change how this API will work in the next major version. + + With the next major version the `beforeSendSpan` API can only be used to mutate spans, but no longer to drop them. + With this release the SDK will warn you if you are using this API to drop spans. + Instead, it is recommended to configure instrumentation (i.e. integrations) directly to control what spans are created. + + Additionally, with the next major version, root spans will also be passed to `beforeSendSpan`. + +- **feat(utils): Deprecate `@sentry/utils` ([#14431](https://github.com/getsentry/sentry-javascript/pull/14431))** + + With the next major version the `@sentry/utils` package will be merged into the `@sentry/core` package. + It is therefore no longer recommended to use the `@sentry/utils` package. + +- **feat(vue): Deprecate configuring Vue tracing options anywhere else other than through the `vueIntegration`'s `tracingOptions` option ([#14385](https://github.com/getsentry/sentry-javascript/pull/14385))** + + Currently it is possible to configure tracing options in various places in the Sentry Vue SDK: + + - In `Sentry.init()` + - Inside `tracingOptions` in `Sentry.init()` + - In the `vueIntegration()` options + - Inside `tracingOptions` in the `vueIntegration()` options + + Because this is a bit messy and confusing to document, the only recommended way to configure tracing options going forward is through the `tracingOptions` in the `vueIntegration()`. + The other means of configuration will be removed in the next major version of the SDK. + +- **feat: Deprecate `registerEsmLoaderHooks.include` and `registerEsmLoaderHooks.exclude` ([#14486](https://github.com/getsentry/sentry-javascript/pull/14486))** + + Currently it is possible to define `registerEsmLoaderHooks.include` and `registerEsmLoaderHooks.exclude` options in `Sentry.init()` to only apply ESM loader hooks to a subset of modules. + This API served as an escape hatch in case certain modules are incompatible with ESM loader hooks. + + Since this API was introduced, a way was found to only wrap modules that there exists instrumentation for (meaning a vetted list). + To only wrap modules that have instrumentation, it is recommended to instead set `registerEsmLoaderHooks.onlyIncludeInstrumentedModules` to `true`. + + Note that `onlyIncludeInstrumentedModules: true` will become the default behavior in the next major version and the `registerEsmLoaderHooks` will no longer accept fine-grained options. + +The following deprecations will _most likely_ not affect you unless you are building an SDK yourself: + +- feat(core): Deprecate `arrayify` ([#14405](https://github.com/getsentry/sentry-javascript/pull/14405)) +- feat(core): Deprecate `flatten` ([#14454](https://github.com/getsentry/sentry-javascript/pull/14454)) +- feat(core): Deprecate `urlEncode` ([#14406](https://github.com/getsentry/sentry-javascript/pull/14406)) +- feat(core): Deprecate `validSeverityLevels` ([#14407](https://github.com/getsentry/sentry-javascript/pull/14407)) +- feat(core/utils): Deprecate `getNumberOfUrlSegments` ([#14458](https://github.com/getsentry/sentry-javascript/pull/14458)) +- feat(utils): Deprecate `memoBuilder`, `BAGGAGE_HEADER_NAME`, and `makeFifoCache` ([#14434](https://github.com/getsentry/sentry-javascript/pull/14434)) +- feat(utils/core): Deprecate `addRequestDataToEvent` and `extractRequestData` ([#14430](https://github.com/getsentry/sentry-javascript/pull/14430)) + +### Other Changes + +- feat: Streamline `sentry-trace`, `baggage` and DSC handling ([#14364](https://github.com/getsentry/sentry-javascript/pull/14364)) +- feat(core): Further optimize debug ID parsing ([#14365](https://github.com/getsentry/sentry-javascript/pull/14365)) +- feat(node): Add `openTelemetryInstrumentations` option ([#14484](https://github.com/getsentry/sentry-javascript/pull/14484)) +- feat(nuxt): Add filter for not found source maps (devtools) ([#14437](https://github.com/getsentry/sentry-javascript/pull/14437)) +- feat(nuxt): Only delete public source maps ([#14438](https://github.com/getsentry/sentry-javascript/pull/14438)) +- fix(nextjs): Don't report `NEXT_REDIRECT` from browser ([#14440](https://github.com/getsentry/sentry-javascript/pull/14440)) +- perf(opentelemetry): Bucket spans for cleanup ([#14154](https://github.com/getsentry/sentry-javascript/pull/14154)) + +Work in this release was contributed by @NEKOYASAN and @fmorett. Thank you for your contributions! + ## 8.40.0 ### Important Changes diff --git a/dev-packages/browser-integration-tests/suites/integrations/ContextLines/noAddedLines/test.ts b/dev-packages/browser-integration-tests/suites/integrations/ContextLines/noAddedLines/test.ts index 8fa8ec16cddd..a3c8614bfb2b 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/ContextLines/noAddedLines/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/ContextLines/noAddedLines/test.ts @@ -7,6 +7,7 @@ sentryTest('should not add source context lines to errors from script files', as const url = await getLocalTestUrl({ testDir: __dirname }); const eventReqPromise = waitForErrorRequestOnUrl(page, url); + await page.waitForFunction('window.Sentry'); const clickPromise = page.locator('#script-error-btn').click(); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/subject.js new file mode 100644 index 000000000000..1fa6434c2634 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/subject.js @@ -0,0 +1,4 @@ +Sentry.withScope(() => { + Sentry.startSpan({ name: 'test_span_1' }, () => undefined); + Sentry.startSpan({ name: 'test_span_2' }, () => undefined); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts new file mode 100644 index 000000000000..8ad6e31eccce --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; + +import type { TransactionEvent } from '@sentry/types'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should send manually started parallel root spans outside of root context', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1'); + const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2'); + + await page.goto(url); + + const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]); + + const transaction1 = envelopeRequestParser(transaction1Req); + const transaction2 = envelopeRequestParser(transaction2Req); + + expect(transaction1).toBeDefined(); + expect(transaction2).toBeDefined(); + + const trace1Id = transaction1.contexts?.trace?.trace_id; + const trace2Id = transaction2.contexts?.trace?.trace_id; + + expect(trace1Id).toBeDefined(); + expect(trace2Id).toBeDefined(); + + // We use the same traceID from the root propagation context here + expect(trace1Id).toBe(trace2Id); + + expect(transaction1.contexts?.trace?.parent_span_id).toBeUndefined(); + expect(transaction2.contexts?.trace?.parent_span_id).toBeUndefined(); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js new file mode 100644 index 000000000000..56c0e05a269c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js @@ -0,0 +1,8 @@ +Sentry.getCurrentScope().setPropagationContext({ + parentSpanId: '1234567890123456', + spanId: '123456789012345x', + traceId: '12345678901234567890123456789012', +}); + +Sentry.startSpan({ name: 'test_span_1' }, () => undefined); +Sentry.startSpan({ name: 'test_span_2' }, () => undefined); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts new file mode 100644 index 000000000000..212e4808f3e7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; + +import type { TransactionEvent } from '@sentry/types'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should send manually started parallel root spans in root context with parentSpanId', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1'); + const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2'); + + await page.goto(url); + + const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]); + + const transaction1 = envelopeRequestParser(transaction1Req); + const transaction2 = envelopeRequestParser(transaction2Req); + + expect(transaction1).toBeDefined(); + expect(transaction2).toBeDefined(); + + const trace1Id = transaction1.contexts?.trace?.trace_id; + const trace2Id = transaction2.contexts?.trace?.trace_id; + + expect(trace1Id).toBe('12345678901234567890123456789012'); + expect(trace2Id).toBe('12345678901234567890123456789012'); + + expect(transaction1.contexts?.trace?.parent_span_id).toBe('1234567890123456'); + expect(transaction2.contexts?.trace?.parent_span_id).toBe('1234567890123456'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/subject.js new file mode 100644 index 000000000000..b07ba4e8aab2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/subject.js @@ -0,0 +1,2 @@ +Sentry.startSpan({ name: 'test_span_1' }, () => undefined); +Sentry.startSpan({ name: 'test_span_2' }, () => undefined); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts new file mode 100644 index 000000000000..47e8e9fa3ac5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; + +import type { TransactionEvent } from '@sentry/types'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('should send manually started parallel root spans in root context', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1'); + const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2'); + + await page.goto(url); + + const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]); + + const transaction1 = envelopeRequestParser(transaction1Req); + const transaction2 = envelopeRequestParser(transaction2Req); + + expect(transaction1).toBeDefined(); + expect(transaction2).toBeDefined(); + + const trace1Id = transaction1.contexts?.trace?.trace_id; + const trace2Id = transaction2.contexts?.trace?.trace_id; + + expect(trace1Id).toBeDefined(); + expect(trace2Id).toBeDefined(); + + // We use the same traceID from the root propagation context here + expect(trace1Id).toBe(trace2Id); + + expect(transaction1.contexts?.trace?.parent_span_id).toBeUndefined(); + expect(transaction2.contexts?.trace?.parent_span_id).toBeUndefined(); +}); diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index 01d92a07a8e5..34e591cad0dc 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -7,6 +7,7 @@ import type { Event, EventEnvelope, EventEnvelopeHeaders, + TransactionEvent, } from '@sentry/types'; export const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//; @@ -224,7 +225,10 @@ export function waitForErrorRequest(page: Page, callback?: (event: Event) => boo }); } -export function waitForTransactionRequest(page: Page): Promise { +export function waitForTransactionRequest( + page: Page, + callback?: (event: TransactionEvent) => boolean, +): Promise { return page.waitForRequest(req => { const postData = req.postData(); if (!postData) { @@ -234,7 +238,15 @@ export function waitForTransactionRequest(page: Page): Promise { try { const event = envelopeRequestParser(req); - return event.type === 'transaction'; + if (event.type !== 'transaction') { + return false; + } + + if (callback) { + return callback(event as TransactionEvent); + } + + return true; } catch { return false; } diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 160c27c6fad4..579a22f41d31 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -16,7 +16,7 @@ "clean": "rimraf tmp node_modules pnpm-lock.yaml && yarn clean:test-applications", "ci:build-matrix": "ts-node ./lib/getTestMatrix.ts", "ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true", - "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.sveltekit,pnpm-lock.yaml} .last-run.json && pnpm store prune" + "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.sveltekit,pnpm-lock.yaml,.last-run.json,test-results} && pnpm store prune" }, "devDependencies": { "@types/glob": "8.0.0", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/destination/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/destination/page.tsx new file mode 100644 index 000000000000..5583d36b04b0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/destination/page.tsx @@ -0,0 +1,7 @@ +export default function RedirectDestinationPage() { + return ( +
+

Redirect Destination

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/origin/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/origin/page.tsx new file mode 100644 index 000000000000..52615e0a054b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/origin/page.tsx @@ -0,0 +1,18 @@ +import { redirect } from 'next/navigation'; + +async function redirectAction() { + 'use server'; + + redirect('/redirect/destination'); +} + +export default function RedirectOriginPage() { + return ( + <> + {/* @ts-ignore */} +
+ +
+ + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/server-action-redirect.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/server-action-redirect.test.ts new file mode 100644 index 000000000000..d46936fa6b2f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/server-action-redirect.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should handle server action redirect without capturing errors', async ({ page }) => { + // Wait for the initial page load transaction + const pageLoadTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === '/redirect/origin'; + }); + + // Navigate to the origin page + await page.goto('/redirect/origin'); + + const pageLoadTransaction = await pageLoadTransactionPromise; + expect(pageLoadTransaction).toBeDefined(); + + // Wait for the redirect transaction + const redirectTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'GET /redirect/destination'; + }); + + // No error should be captured + const redirectErrorPromise = waitForError('nextjs-15', async errorEvent => { + return !!errorEvent; + }); + + // Click the redirect button + await page.click('button[type="submit"]'); + + await redirectTransactionPromise; + + // Verify we got redirected to the destination page + await expect(page).toHaveURL('/redirect/destination'); + + // Wait for potential errors with a 2 second timeout + const errorTimeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('No error captured (timeout)')), 2000), + ); + + // We expect this to timeout since no error should be captured during the redirect + try { + await Promise.race([redirectErrorPromise, errorTimeout]); + throw new Error('Expected no error to be captured, but an error was found'); + } catch (e) { + // If we get a timeout error (as expected), no error was captured + expect((e as Error).message).toBe('No error captured (timeout)'); + } +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/very-slow-component/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/very-slow-component/page.tsx index ab40d1e62d5f..79e534a9e89e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/very-slow-component/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/very-slow-component/page.tsx @@ -1,6 +1,6 @@ export const dynamic = 'force-dynamic'; export default async function SuperSlowPage() { - await new Promise(resolve => setTimeout(resolve, 10000)); + await new Promise(resolve => setTimeout(resolve, 5000)); return null; } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index 278b6b1074eb..cc22b9da1a40 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -125,13 +125,14 @@ test('Should set not_found status for server actions calling notFound()', async test('Will not include spans in pageload transaction with faulty timestamps for slow loading pages', async ({ page, }) => { + test.slow(); const pageloadTransactionEventPromise = waitForTransaction('nextjs-app-dir', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/very-slow-component' ); }); - await page.goto('/very-slow-component'); + await page.goto('/very-slow-component', { timeout: 11000 }); const pageLoadTransaction = await pageloadTransactionEventPromise; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc index 070f80f05092..c0bee06878d1 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc @@ -1,2 +1,8 @@ @sentry:registry=http://127.0.0.1:4873 @sentry-internal:registry=http://127.0.0.1:4873 + +# todo: check if this is still needed in upcoming versions +# Hoist all dependencies to the root level due to issues with import-in-the-middle and require-in-the-middle +# Just adding these as dependencies removed the warnings, but didn't fix the issue +shamefully-hoist=true +strict-peer-dependencies=false diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json index 18f798f89246..34180346b252 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json @@ -1,6 +1,6 @@ { "name": "nuxt-3-min", - "description": "E2E test app for the minimum nuxt 3 version our nuxt SDK supports.", + "description": "E2E test app for the minimum Nuxt 3 version our Nuxt SDK supports.", "private": true, "type": "module", "scripts": { @@ -16,7 +16,7 @@ }, "dependencies": { "@sentry/nuxt": "latest || *", - "nuxt": "3.13.2" + "nuxt": "3.7.0" }, "devDependencies": { "@nuxt/test-utils": "^3.14.1", @@ -24,7 +24,7 @@ "@sentry-internal/test-utils": "link:../../../test-utils" }, "overrides": { - "nitropack": "2.9.7", - "@vercel/nft": "^0.27.4" + "nitropack": "2.10.0", + "ofetch": "1.4.0" } } diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json index 0b9654108d48..8cc66d2d408e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -15,14 +15,11 @@ }, "dependencies": { "@sentry/nuxt": "latest || *", - "nuxt": "^3.13.1" + "nuxt": "^3.14.0" }, "devDependencies": { "@nuxt/test-utils": "^3.14.1", "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils" - }, - "overrides": { - "@vercel/nft": "0.27.4" } } diff --git a/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts b/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts index 25f90460e2b9..f8d752497d46 100644 --- a/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts +++ b/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts @@ -5,7 +5,7 @@ afterAll(() => { }); const esmWarning = - '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or use version 7.x of the Sentry Node.js SDK.'; + '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or upgrade your Node.js version.'; test("warns if using ESM on Node.js versions that don't support `register()`", async () => { const nodeMajorVersion = Number(process.versions.node.split('.')[0]); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts new file mode 100644 index 000000000000..e352fff5c02c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts @@ -0,0 +1,30 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.getCurrentScope().setPropagationContext({ + parentSpanId: '1234567890123456', + spanId: '123456789012345x', + traceId: '12345678901234567890123456789012', +}); + +const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, +); + +Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, +); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts new file mode 100644 index 000000000000..ed7726d72389 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts @@ -0,0 +1,29 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should send manually started parallel root spans in root context', done => { + expect.assertions(7); + + createRunner(__dirname, 'scenario.ts') + .expect({ transaction: { transaction: 'test_span_1' } }) + .expect({ + transaction: transaction => { + expect(transaction).toBeDefined(); + const traceId = transaction.contexts?.trace?.trace_id; + expect(traceId).toBeDefined(); + + // It ignores propagation context of the root context + expect(traceId).not.toBe('12345678901234567890123456789012'); + expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined(); + + // Different trace ID than the first span + const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + expect(trace1Id).not.toBe(traceId); + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts new file mode 100644 index 000000000000..ec761a7d591d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts @@ -0,0 +1,20 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.withScope(scope => { + scope.setPropagationContext({ + parentSpanId: '1234567890123456', + spanId: '123456789012345x', + traceId: '12345678901234567890123456789012', + }); + + Sentry.startSpan({ name: 'test_span_1' }, () => undefined); + Sentry.startSpan({ name: 'test_span_2' }, () => undefined); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts new file mode 100644 index 000000000000..9a561ffd391a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts @@ -0,0 +1,34 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should send manually started parallel root spans outside of root context with parentSpanId', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'test_span_1', + contexts: { + trace: { + span_id: expect.any(String), + parent_span_id: '1234567890123456', + trace_id: '12345678901234567890123456789012', + }, + }, + }, + }) + .expect({ + transaction: { + transaction: 'test_span_2', + contexts: { + trace: { + span_id: expect.any(String), + parent_span_id: '1234567890123456', + trace_id: '12345678901234567890123456789012', + }, + }, + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts new file mode 100644 index 000000000000..69a9d5e2c0ef --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts @@ -0,0 +1,26 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.withScope(() => { + const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, + ); + + Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, + ); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts new file mode 100644 index 000000000000..97ceaa1e382c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts @@ -0,0 +1,27 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should send manually started parallel root spans outside of root context', done => { + expect.assertions(6); + + createRunner(__dirname, 'scenario.ts') + .expect({ transaction: { transaction: 'test_span_1' } }) + .expect({ + transaction: transaction => { + expect(transaction).toBeDefined(); + const traceId = transaction.contexts?.trace?.trace_id; + expect(traceId).toBeDefined(); + expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined(); + + const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + + // Same trace ID as the first span + expect(trace1Id).toBe(traceId); + }, + }) + .start(done); +}); diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index d117d66ecae3..4d289eb56204 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -2,17 +2,47 @@ # Deprecations +## General + +- **Returning `null` from `beforeSendSpan` span is deprecated.** +- **Passing `undefined` to `tracesSampleRate` / `tracesSampler` / `enableTracing` will be handled differently in v9** + + In v8, a setup like the following: + + ```ts + Sentry.init({ + tracesSampleRate: undefined, + }); + ``` + + Will result in tracing being _enabled_, although no spans will be generated. + In v9, we will streamline this behavior so that passing `undefined` will result in tracing being disabled, the same as not passing the option at all. + If you are relying on `undefined` being passed in and having tracing enabled because of this, you should update your config to set e.g. `tracesSampleRate: 0` instead, which will also enable tracing in v9. + ## `@sentry/utils` +- **The `@sentry/utils` package has been deprecated. Import everything from `@sentry/core` instead.** + - Deprecated `AddRequestDataToEventOptions.transaction`. This option effectively doesn't do anything anymore, and will be removed in v9. - Deprecated `TransactionNamingScheme` type. +- Deprecated `validSeverityLevels`. Will not be replaced. +- Deprecated `urlEncode`. No replacements. +- Deprecated `addRequestDataToEvent`. Use `addNormalizedRequestDataToEvent` instead. +- Deprecated `extractRequestData`. Instead manually extract relevant data off request. +- Deprecated `arrayify`. No replacements. +- Deprecated `memoBuilder`. No replacements. +- Deprecated `getNumberOfUrlSegments`. No replacements. +- Deprecated `BAGGAGE_HEADER_NAME`. No replacements. +- Deprecated `makeFifoCache`. No replacements. +- Deprecated `flatten`. No replacements. ## `@sentry/core` - Deprecated `transactionNamingScheme` option in `requestDataIntegration`. - Deprecated `debugIntegration`. To log outgoing events, use [Hook Options](https://docs.sentry.io/platforms/javascript/configuration/options/#hooks) (`beforeSend`, `beforeSendTransaction`, ...). - Deprecated `sessionTimingIntegration`. To capture session durations alongside events, use [Context](https://docs.sentry.io/platforms/javascript/enriching-events/context/) (`Sentry.setContext()`). +- Deprecated `addTracingHeadersToFetchRequest` method - this was only meant for internal use and is not needed anymore. ## `@sentry/nestjs` @@ -34,8 +64,31 @@ - Deprecated `Request` in favor of `RequestEventData`. +## `@sentry/vue` + +- Deprecated `tracingOptions`, `trackComponents`, `timeout`, `hooks` options everywhere other than in the `tracingOptions` option of the `vueIntegration()`. + These options should now be set as follows: + + ```ts + import * as Sentry from '@sentry/vue'; + + Sentry.init({ + integrations: [ + Sentry.vueIntegration({ + tracingOptions: { + trackComponents: true, + timeout: 1000, + hooks: ['mount', 'update', 'unmount'], + }, + }), + ], + }); + ``` + ## Server-side SDKs (`@sentry/node` and all dependents) - Deprecated `processThreadBreadcrumbIntegration` in favor of `childProcessIntegration`. Functionally they are the same. - Deprecated `nestIntegration`. Use the NestJS SDK (`@sentry/nestjs`) instead. - Deprecated `setupNestErrorHandler`. Use the NestJS SDK (`@sentry/nestjs`) instead. +- Deprecated `registerEsmLoaderHooks.include` and `registerEsmLoaderHooks.exclude`. Set `onlyIncludeInstrumentedModules: true` instead. +- `registerEsmLoaderHooks` will only accept `true | false | undefined` in the future. The SDK will default to wrapping modules that are used as part of OpenTelemetry Instrumentation. diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 92e175b9205e..922fdd15dce1 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -12,6 +12,7 @@ export { addEventProcessor, addIntegration, addOpenTelemetryInstrumentation, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, amqplibIntegration, anrIntegration, @@ -39,6 +40,7 @@ export { endSession, expressErrorHandler, expressIntegration, + // eslint-disable-next-line deprecation/deprecation extractRequestData, extraErrorDataIntegration, fastifyIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index e5e38100a257..369e8824a3b9 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -42,8 +42,10 @@ export { flush, close, getSentryRelease, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, DEFAULT_USER_INCLUDES, + // eslint-disable-next-line deprecation/deprecation extractRequestData, createGetModuleFromFilename, anrIntegration, diff --git a/packages/browser-utils/.eslintrc.js b/packages/browser-utils/.eslintrc.js index 9d8a86b13b96..607e5d1b7d43 100644 --- a/packages/browser-utils/.eslintrc.js +++ b/packages/browser-utils/.eslintrc.js @@ -6,9 +6,7 @@ module.exports = { overrides: [ { files: ['src/**'], - rules: { - '@sentry-internal/sdk/no-optional-chaining': 'off', - }, + rules: {}, }, { files: ['src/metrics/**'], diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index d64bba34509a..4352fe3d5f27 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -1,11 +1,10 @@ /* eslint-disable max-lines */ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan } from '@sentry/core'; import { setMeasurement } from '@sentry/core'; -import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger, parseUrl } from '@sentry/core'; +import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, parseUrl } from '@sentry/core'; import type { Measurements, Span, SpanAttributes, StartSpanOptions } from '@sentry/types'; import { spanToJSON } from '@sentry/core'; -import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../types'; import { trackClsAsStandaloneSpan } from './cls'; import { @@ -241,7 +240,6 @@ function _trackCLS(): () => void { if (!entry) { return; } - DEBUG_BUILD && logger.log(`[Measurements] Adding CLS ${metric.value}`); _measurements['cls'] = { value: metric.value, unit: '' }; _clsEntry = entry; }, true); @@ -255,7 +253,6 @@ function _trackLCP(): () => void { return; } - DEBUG_BUILD && logger.log('[Measurements] Adding LCP'); _measurements['lcp'] = { value: metric.value, unit: 'millisecond' }; _lcpEntry = entry as LargestContentfulPaint; }, true); @@ -271,7 +268,6 @@ function _trackFID(): () => void { const timeOrigin = msToSec(browserPerformanceTimeOrigin as number); const startTime = msToSec(entry.startTime); - DEBUG_BUILD && logger.log('[Measurements] Adding FID'); _measurements['fid'] = { value: metric.value, unit: 'millisecond' }; _measurements['mark.fid'] = { value: timeOrigin + startTime, unit: 'second' }; }); @@ -284,7 +280,6 @@ function _trackTtfb(): () => void { return; } - DEBUG_BUILD && logger.log('[Measurements] Adding TTFB'); _measurements['ttfb'] = { value: metric.value, unit: 'millisecond' }; }); } @@ -305,7 +300,6 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries return; } - DEBUG_BUILD && logger.log('[Tracing] Adding & adjusting spans using Performance API'); const timeOrigin = msToSec(browserPerformanceTimeOrigin); const performanceEntries = performance.getEntries(); @@ -343,11 +337,9 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries const shouldRecord = entry.startTime < firstHidden.firstHiddenTime; if (entry.name === 'first-paint' && shouldRecord) { - DEBUG_BUILD && logger.log('[Measurements] Adding FP'); _measurements['fp'] = { value: entry.startTime, unit: 'millisecond' }; } if (entry.name === 'first-contentful-paint' && shouldRecord) { - DEBUG_BUILD && logger.log('[Measurements] Adding FCP'); _measurements['fcp'] = { value: entry.startTime, unit: 'millisecond' }; } break; @@ -618,8 +610,6 @@ function _trackNavigator(span: Span): void { /** Add LCP / CLS data to span to allow debugging */ function _setWebVitalAttributes(span: Span): void { if (_lcpEntry) { - DEBUG_BUILD && logger.log('[Measurements] Adding LCP Data'); - // Capture Properties of the LCP element that contributes to the LCP. if (_lcpEntry.element) { @@ -652,7 +642,6 @@ function _setWebVitalAttributes(span: Span): void { // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift if (_clsEntry && _clsEntry.sources) { - DEBUG_BUILD && logger.log('[Measurements] Adding CLS Data'); _clsEntry.sources.forEach((source, index) => span.setAttribute(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), ); @@ -685,7 +674,6 @@ function _addTtfbRequestTimeToMeasurements(_measurements: Measurements): void { const { responseStart, requestStart } = navEntry; if (requestStart <= responseStart) { - DEBUG_BUILD && logger.log('[Measurements] Adding TTFB Request Time'); _measurements['ttfb.requestTime'] = { value: responseStart - requestStart, unit: 'millisecond', diff --git a/packages/browser-utils/src/metrics/cls.ts b/packages/browser-utils/src/metrics/cls.ts index 1a7be525c3aa..05d2f934eb8c 100644 --- a/packages/browser-utils/src/metrics/cls.ts +++ b/packages/browser-utils/src/metrics/cls.ts @@ -67,7 +67,11 @@ export function trackClsAsStandaloneSpan(): void { setTimeout(() => { const client = getClient(); - const unsubscribeStartNavigation = client?.on('startNavigationSpan', () => { + if (!client) { + return; + } + + const unsubscribeStartNavigation = client.on('startNavigationSpan', () => { _collectClsOnce(); unsubscribeStartNavigation && unsubscribeStartNavigation(); }); @@ -84,15 +88,15 @@ export function trackClsAsStandaloneSpan(): void { function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined, pageloadSpanId: string) { DEBUG_BUILD && logger.log(`Sending CLS span (${clsValue})`); - const startTime = msToSec((browserPerformanceTimeOrigin || 0) + (entry?.startTime || 0)); + const startTime = msToSec((browserPerformanceTimeOrigin || 0) + ((entry && entry.startTime) || 0)); const routeName = getCurrentScope().getScopeData().transactionName; - const name = entry ? htmlTreeAsString(entry.sources[0]?.node) : 'Layout shift'; + const name = entry ? htmlTreeAsString(entry.sources[0] && entry.sources[0].node) : 'Layout shift'; const attributes: SpanAttributes = dropUndefinedKeys({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.cls', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.webvital.cls', - [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry?.duration || 0, + [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: (entry && entry.duration) || 0, // attach the pageload span id to the CLS span so that we can link them in the UI 'sentry.pageload.span_id': pageloadSpanId, }); @@ -104,19 +108,21 @@ function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined, startTime, }); - span?.addEvent('cls', { - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: '', - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: clsValue, - }); + if (span) { + span.addEvent('cls', { + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: '', + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: clsValue, + }); - // LayoutShift performance entries always have a duration of 0, so we don't need to add `entry.duration` here - // see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/duration - span?.end(startTime); + // LayoutShift performance entries always have a duration of 0, so we don't need to add `entry.duration` here + // see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/duration + span.end(startTime); + } } function supportsLayoutShift(): boolean { try { - return PerformanceObserver.supportedEntryTypes?.includes('layout-shift'); + return PerformanceObserver.supportedEntryTypes.includes('layout-shift'); } catch { return false; } diff --git a/packages/browser-utils/src/metrics/inp.ts b/packages/browser-utils/src/metrics/inp.ts index c722e6425c3a..4e90c89619d9 100644 --- a/packages/browser-utils/src/metrics/inp.ts +++ b/packages/browser-utils/src/metrics/inp.ts @@ -112,12 +112,14 @@ function _trackINP(): () => void { startTime, }); - span?.addEvent('inp', { - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond', - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value, - }); + if (span) { + span.addEvent('inp', { + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond', + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value, + }); - span?.end(startTime + duration); + span.end(startTime + duration); + } }); } diff --git a/packages/browser-utils/src/metrics/web-vitals/getINP.ts b/packages/browser-utils/src/metrics/web-vitals/getINP.ts index 96558f2cf109..e66f17eed2a1 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getINP.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getINP.ts @@ -66,7 +66,6 @@ const processEntry = (entry: PerformanceEventTiming) => { // The least-long of the 10 longest interactions. const minLongestInteraction = longestInteractionList[longestInteractionList.length - 1]; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const existingInteraction = longestInteractionMap[entry.interactionId!]; // Only process the entry if it's possibly one of the ten longest, @@ -82,7 +81,6 @@ const processEntry = (entry: PerformanceEventTiming) => { existingInteraction.latency = Math.max(existingInteraction.latency, entry.duration); } else { const interaction = { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion id: entry.interactionId!, latency: entry.duration, entries: [entry], diff --git a/packages/browser-utils/test/utils/TestClient.ts b/packages/browser-utils/test/utils/TestClient.ts index 3bd5fff8cf76..b3e3c29cb8cd 100644 --- a/packages/browser-utils/test/utils/TestClient.ts +++ b/packages/browser-utils/test/utils/TestClient.ts @@ -20,10 +20,8 @@ export class TestClient extends BaseClient { exception: { values: [ { - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ type: exception.name, value: exception.message, - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ }, ], }, diff --git a/packages/browser/src/integrations/httpclient.ts b/packages/browser/src/integrations/httpclient.ts index 439941c97faf..88c1418ab4af 100644 --- a/packages/browser/src/integrations/httpclient.ts +++ b/packages/browser/src/integrations/httpclient.ts @@ -108,8 +108,8 @@ function _parseCookieHeaders( if (cookieString) { cookies = _parseCookieString(cookieString); } - } catch (e) { - DEBUG_BUILD && logger.log(`Could not extract cookies from header ${cookieHeader}`); + } catch { + // ignore it if parsing fails } return [headers, cookies]; @@ -138,14 +138,14 @@ function _xhrResponseHandler( if (cookieString) { responseCookies = _parseCookieString(cookieString); } - } catch (e) { - DEBUG_BUILD && logger.log('Could not extract cookies from response headers'); + } catch { + // ignore it if parsing fails } try { responseHeaders = _getXHRResponseHeaders(xhr); - } catch (e) { - DEBUG_BUILD && logger.log('Could not extract headers from response'); + } catch { + // ignore it if parsing fails } requestHeaders = headers; diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index a59706edb8fb..37d3b532129c 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -299,16 +299,20 @@ export const browserTracingIntegration = ((_options: Partial { if (getClient() !== client) { return; } - if (activeSpan && !spanToJSON(activeSpan).timestamp) { - DEBUG_BUILD && logger.log(`[Tracing] Finishing current root span 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(); - } + maybeEndActiveSpan(); activeSpan = _createRouteSpan(client, { op: 'navigation', @@ -320,12 +324,7 @@ export const browserTracingIntegration = ((_options: Partial = { + sentry_version: SENTRY_API_VERSION, + }; + + if (dsn.publicKey) { // We send only the minimum set of required information. See // https://github.com/getsentry/sentry-javascript/issues/2572. - sentry_key: dsn.publicKey, - sentry_version: SENTRY_API_VERSION, - ...(sdkInfo && { sentry_client: `${sdkInfo.name}/${sdkInfo.version}` }), - }); + params.sentry_key = dsn.publicKey; + } + + if (sdkInfo) { + params.sentry_client = `${sdkInfo.name}/${sdkInfo.version}`; + } + + return new URLSearchParams(params).toString(); } /** diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 840992c4ea79..23010505a7dc 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -32,7 +32,7 @@ import type { } from '@sentry/types'; import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; -import { getIsolationScope } from './currentScopes'; +import { getCurrentScope, getIsolationScope, getTraceContextFromScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; import type { IntegrationIndex } from './integration'; @@ -40,18 +40,18 @@ import { afterSetupIntegrations } from './integration'; import { setupIntegration, setupIntegrations } from './integration'; import type { Scope } from './scope'; import { updateSession } from './session'; -import { getDynamicSamplingContextFromClient } from './tracing/dynamicSamplingContext'; +import { getDynamicSamplingContextFromScope } from './tracing/dynamicSamplingContext'; import { createClientReportEnvelope } from './utils-hoist/clientreport'; import { dsnToString, makeDsn } from './utils-hoist/dsn'; import { addItemToEnvelope, createAttachmentEnvelopeItem } from './utils-hoist/envelope'; import { SentryError } from './utils-hoist/error'; import { isParameterizedString, isPlainObject, isPrimitive, isThenable } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; +import { consoleSandbox, logger } from './utils-hoist/logger'; import { checkOrSetAlreadyCaught, uuid4 } from './utils-hoist/misc'; -import { dropUndefinedKeys } from './utils-hoist/object'; import { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils-hoist/syncpromise'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; +import { showSpanDropWarning } from './utils/spanUtils'; const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; @@ -141,6 +141,18 @@ export abstract class BaseClient implements Client { url, }); } + + // TODO(v9): Remove this deprecation warning + const tracingOptions = ['enableTracing', 'tracesSampleRate', 'tracesSampler'] as const; + const undefinedOption = tracingOptions.find(option => option in options && options[option] == undefined); + if (undefinedOption) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] Deprecation warning: \`${undefinedOption}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`, + ); + }); + } } /** @@ -659,7 +671,7 @@ export abstract class BaseClient implements Client { protected _prepareEvent( event: Event, hint: EventHint, - currentScope?: Scope, + currentScope = getCurrentScope(), isolationScope = getIsolationScope(), ): PromiseLike { const options = this.getOptions(); @@ -679,30 +691,18 @@ export abstract class BaseClient implements Client { return evt; } - const propagationContext = { - ...isolationScope.getPropagationContext(), - ...(currentScope ? currentScope.getPropagationContext() : undefined), + evt.contexts = { + trace: getTraceContextFromScope(currentScope), + ...evt.contexts, }; - const trace = evt.contexts && evt.contexts.trace; - if (!trace && propagationContext) { - const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext; - evt.contexts = { - trace: dropUndefinedKeys({ - trace_id, - span_id: spanId, - parent_span_id: parentSpanId, - }), - ...evt.contexts, - }; + const dynamicSamplingContext = getDynamicSamplingContextFromScope(this, currentScope); - const dynamicSamplingContext = dsc ? dsc : getDynamicSamplingContextFromClient(trace_id, this); + evt.sdkProcessingMetadata = { + dynamicSamplingContext, + ...evt.sdkProcessingMetadata, + }; - evt.sdkProcessingMetadata = { - dynamicSamplingContext, - ...evt.sdkProcessingMetadata, - }; - } return evt; }); } @@ -977,6 +977,7 @@ function processBeforeSend( if (processedSpan) { processedSpans.push(processedSpan); } else { + showSpanDropWarning(); client.recordDroppedEvent('before_send', 'span'); } } diff --git a/packages/core/src/currentScopes.ts b/packages/core/src/currentScopes.ts index 77e8aa70a02a..825092fb2d5d 100644 --- a/packages/core/src/currentScopes.ts +++ b/packages/core/src/currentScopes.ts @@ -1,8 +1,9 @@ -import type { Scope } from '@sentry/types'; +import type { Scope, TraceContext } from '@sentry/types'; import type { Client } from '@sentry/types'; import { getAsyncContextStrategy } from './asyncContext'; import { getMainCarrier } from './carrier'; import { Scope as ScopeClass } from './scope'; +import { dropUndefinedKeys } from './utils-hoist/object'; import { getGlobalSingleton } from './utils-hoist/worldwide'; /** @@ -120,3 +121,20 @@ export function withIsolationScope( export function getClient(): C | undefined { return getCurrentScope().getClient(); } + +/** + * Get a trace context for the given scope. + */ +export function getTraceContextFromScope(scope: Scope): TraceContext { + const propagationContext = scope.getPropagationContext(); + + const { traceId, spanId, parentSpanId } = propagationContext; + + const traceContext: TraceContext = dropUndefinedKeys({ + trace_id: traceId, + span_id: spanId, + parent_span_id: parentSpanId, + }); + + return traceContext; +} diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index ddeb2ce21997..8ecdeb0d604f 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -24,7 +24,7 @@ import { createSpanEnvelopeItem, getSdkMetadataForEnvelopeHeader, } from './utils-hoist/envelope'; -import { spanToJSON } from './utils/spanUtils'; +import { showSpanDropWarning, spanToJSON } from './utils/spanUtils'; /** * Apply SdkInfo (name, version, packages, integrations) to the corresponding event key. @@ -122,7 +122,13 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? const beforeSendSpan = client && client.getOptions().beforeSendSpan; const convertToSpanJSON = beforeSendSpan - ? (span: SentrySpan) => beforeSendSpan(spanToJSON(span) as SpanJSON) + ? (span: SentrySpan) => { + const spanJson = beforeSendSpan(spanToJSON(span) as SpanJSON); + if (!spanJson) { + showSpanDropWarning(); + } + return spanJson; + } : (span: SentrySpan) => spanToJSON(span); const items: SpanItem[] = []; diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 5880d2594baa..1dbb84423678 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -1,24 +1,13 @@ import type { Client, HandlerDataFetch, Scope, Span, SpanOrigin } from '@sentry/types'; -import { getClient, getCurrentScope, getIsolationScope } from './currentScopes'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes'; -import { - SPAN_STATUS_ERROR, - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - setHttpStatus, - startInactiveSpan, -} from './tracing'; +import { SPAN_STATUS_ERROR, setHttpStatus, startInactiveSpan } from './tracing'; import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; -import { - BAGGAGE_HEADER_NAME, - SENTRY_BAGGAGE_KEY_PREFIX, - dynamicSamplingContextToSentryBaggageHeader, -} from './utils-hoist/baggage'; +import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils-hoist/baggage'; import { isInstanceOf } from './utils-hoist/is'; -import { generateSentryTraceHeader } from './utils-hoist/tracing'; import { parseUrl } from './utils-hoist/url'; import { hasTracingEnabled } from './utils/hasTracingEnabled'; -import { getActiveSpan, spanToTraceHeader } from './utils/spanUtils'; +import { getActiveSpan } from './utils/spanUtils'; +import { getTraceData } from './utils/traceData'; type PolymorphicRequestHeaders = | Record @@ -63,9 +52,6 @@ export function instrumentFetchRequest( return undefined; } - const scope = getCurrentScope(); - const client = getClient(); - const { method, url } = handlerData.fetchData; const fullUrl = getFullURL(url); @@ -92,37 +78,34 @@ export function instrumentFetchRequest( handlerData.fetchData.__span = span.spanContext().spanId; spans[span.spanContext().spanId] = span; - if (shouldAttachHeaders(handlerData.fetchData.url) && client) { + if (shouldAttachHeaders(handlerData.fetchData.url)) { const request: string | Request = handlerData.args[0]; - // In case the user hasn't set the second argument of a fetch call we default it to `{}`. - handlerData.args[1] = handlerData.args[1] || {}; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const options: { [key: string]: any } = handlerData.args[1]; + const options: { [key: string]: unknown } = handlerData.args[1] || {}; - options.headers = addTracingHeadersToFetchRequest( + const headers = _addTracingHeadersToFetchRequest( request, - client, - scope, options, // If performance is disabled (TWP) or there's no active root span (pageload/navigation/interaction), // we do not want to use the span as base for the trace headers, // which means that the headers will be generated from the scope and the sampling decision is deferred hasTracingEnabled() && hasParent ? span : undefined, ); + if (headers) { + // Ensure this is actually set, if no options have been passed previously + handlerData.args[1] = options; + options.headers = headers; + } } return span; } /** - * Adds sentry-trace and baggage headers to the various forms of fetch headers + * Adds sentry-trace and baggage headers to the various forms of fetch headers. */ -export function addTracingHeadersToFetchRequest( - request: string | unknown, // unknown is actually type Request but we can't export DOM types from this package, - client: Client, - scope: Scope, +function _addTracingHeadersToFetchRequest( + request: string | Request, fetchOptionsObj: { headers?: | { @@ -132,48 +115,39 @@ export function addTracingHeadersToFetchRequest( }, span?: Span, ): PolymorphicRequestHeaders | undefined { - const isolationScope = getIsolationScope(); + const traceHeaders = getTraceData({ span }); + const sentryTrace = traceHeaders['sentry-trace']; + const baggage = traceHeaders.baggage; - const { traceId, spanId, sampled, dsc } = { - ...isolationScope.getPropagationContext(), - ...scope.getPropagationContext(), - }; - - const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled); - - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( - dsc || (span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromClient(traceId, client)), - ); + // Nothing to do, when we return undefined here, the original headers will be used + if (!sentryTrace) { + return undefined; + } - const headers = - fetchOptionsObj.headers || - (typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request as Request).headers : undefined); + const headers = fetchOptionsObj.headers || (isRequest(request) ? request.headers : undefined); if (!headers) { - return { 'sentry-trace': sentryTraceHeader, baggage: sentryBaggageHeader }; - } else if (typeof Headers !== 'undefined' && isInstanceOf(headers, Headers)) { - const newHeaders = new Headers(headers as Headers); - - newHeaders.set('sentry-trace', sentryTraceHeader); + return { ...traceHeaders }; + } else if (isHeaders(headers)) { + const newHeaders = new Headers(headers); + newHeaders.set('sentry-trace', sentryTrace); - if (sentryBaggageHeader) { - const prevBaggageHeader = newHeaders.get(BAGGAGE_HEADER_NAME); + if (baggage) { + const prevBaggageHeader = newHeaders.get('baggage'); if (prevBaggageHeader) { const prevHeaderStrippedFromSentryBaggage = stripBaggageHeaderOfSentryBaggageValues(prevBaggageHeader); newHeaders.set( - BAGGAGE_HEADER_NAME, + 'baggage', // If there are non-sentry entries (i.e. if the stripped string is non-empty/truthy) combine the stripped header and sentry baggage header // otherwise just set the sentry baggage header - prevHeaderStrippedFromSentryBaggage - ? `${prevHeaderStrippedFromSentryBaggage},${sentryBaggageHeader}` - : sentryBaggageHeader, + prevHeaderStrippedFromSentryBaggage ? `${prevHeaderStrippedFromSentryBaggage},${baggage}` : baggage, ); } else { - newHeaders.set(BAGGAGE_HEADER_NAME, sentryBaggageHeader); + newHeaders.set('baggage', baggage); } } - return newHeaders as PolymorphicRequestHeaders; + return newHeaders; } else if (Array.isArray(headers)) { const newHeaders = [ ...headers @@ -183,7 +157,7 @@ export function addTracingHeadersToFetchRequest( }) // Get rid of previous sentry baggage values in baggage header .map(header => { - if (Array.isArray(header) && header[0] === BAGGAGE_HEADER_NAME && typeof header[1] === 'string') { + if (Array.isArray(header) && header[0] === 'baggage' && typeof header[1] === 'string') { const [headerName, headerValue, ...rest] = header; return [headerName, stripBaggageHeaderOfSentryBaggageValues(headerValue), ...rest]; } else { @@ -191,13 +165,13 @@ export function addTracingHeadersToFetchRequest( } }), // Attach the new sentry-trace header - ['sentry-trace', sentryTraceHeader], + ['sentry-trace', sentryTrace], ]; - if (sentryBaggageHeader) { + if (baggage) { // If there are multiple entries with the same key, the browser will merge the values into a single request header. // Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header. - newHeaders.push([BAGGAGE_HEADER_NAME, sentryBaggageHeader]); + newHeaders.push(['baggage', baggage]); } return newHeaders as PolymorphicRequestHeaders; @@ -215,18 +189,39 @@ export function addTracingHeadersToFetchRequest( newBaggageHeaders.push(stripBaggageHeaderOfSentryBaggageValues(existingBaggageHeader)); } - if (sentryBaggageHeader) { - newBaggageHeaders.push(sentryBaggageHeader); + if (baggage) { + newBaggageHeaders.push(baggage); } return { ...(headers as Exclude), - 'sentry-trace': sentryTraceHeader, + 'sentry-trace': sentryTrace, baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined, }; } } +/** + * Adds sentry-trace and baggage headers to the various forms of fetch headers. + * + * @deprecated This function will not be exported anymore in v9. + */ +export function addTracingHeadersToFetchRequest( + request: string | unknown, + _client: Client | undefined, + _scope: Scope | undefined, + fetchOptionsObj: { + headers?: + | { + [key: string]: string[] | string | undefined; + } + | PolymorphicRequestHeaders; + }, + span?: Span, +): PolymorphicRequestHeaders | undefined { + return _addTracingHeadersToFetchRequest(request as Request, fetchOptionsObj, span); +} + function getFullURL(url: string): string | undefined { try { const parsed = new URL(url); @@ -264,3 +259,11 @@ function stripBaggageHeaderOfSentryBaggageValues(baggageHeader: string): string .join(',') ); } + +function isRequest(request: unknown): request is Request { + return typeof Request !== 'undefined' && isInstanceOf(request, Request); +} + +function isHeaders(headers: unknown): headers is Headers { + return typeof Headers !== 'undefined' && isInstanceOf(headers, Headers); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c56c568d092f..eed487e961ba 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -40,6 +40,7 @@ export { withScope, withIsolationScope, getClient, + getTraceContextFromScope, } from './currentScopes'; export { getDefaultCurrentScope, @@ -111,7 +112,11 @@ export type { MetricData } from '@sentry/types'; export { metricsDefault } from './metrics/exports-default'; export { BrowserMetricsAggregator } from './metrics/browser-aggregator'; export { getMetricSummaryJsonForSpan } from './metrics/metric-summary'; -export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch'; +export { + // eslint-disable-next-line deprecation/deprecation + addTracingHeadersToFetchRequest, + instrumentFetchRequest, +} from './fetch'; export { trpcMiddleware } from './trpc'; export { captureFeedback } from './feedback'; diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index ae016959d9c3..cce86a8966c8 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -98,6 +98,7 @@ const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) = return event; } + // eslint-disable-next-line deprecation/deprecation return addRequestDataToEvent(event, request, addRequestDataOptions); }, }; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 93c1051ed6ec..c044c1be7bc1 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -14,12 +14,12 @@ import type { import { BaseClient } from './baseclient'; import { createCheckInEnvelope } from './checkin'; -import { getIsolationScope } from './currentScopes'; +import { getIsolationScope, getTraceContextFromScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { Scope } from './scope'; import { SessionFlusher } from './sessionflusher'; import { - getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan, registerSpanErrorInstrumentation, } from './tracing'; @@ -28,7 +28,7 @@ import { logger } from './utils-hoist/logger'; import { uuid4 } from './utils-hoist/misc'; import { resolvedSyncPromise } from './utils-hoist/syncpromise'; import { _getSpanForScope } from './utils/spanOnScope'; -import { getRootSpan, spanToTraceContext } from './utils/spanUtils'; +import { spanToTraceContext } from './utils/spanUtils'; export interface ServerRuntimeClientOptions extends ClientOptions { platform?: string; @@ -251,7 +251,7 @@ export class ServerRuntimeClient< } /** Extract trace information from scope */ - private _getTraceInfoFromScope( + protected _getTraceInfoFromScope( scope: Scope | undefined, ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { if (!scope) { @@ -259,22 +259,11 @@ export class ServerRuntimeClient< } const span = _getSpanForScope(scope); - if (span) { - const rootSpan = getRootSpan(span); - const samplingContext = getDynamicSamplingContextFromSpan(rootSpan); - return [samplingContext, spanToTraceContext(rootSpan)]; - } - - const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext(); - const traceContext: TraceContext = { - trace_id: traceId, - span_id: spanId, - parent_span_id: parentSpanId, - }; - if (dsc) { - return [dsc, traceContext]; - } - return [getDynamicSamplingContextFromClient(traceId, this), traceContext]; + const traceContext = span ? spanToTraceContext(span) : getTraceContextFromScope(scope); + const dynamicSamplingContext = span + ? getDynamicSamplingContextFromSpan(span) + : getDynamicSamplingContextFromScope(this, scope); + return [dynamicSamplingContext, traceContext]; } } diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index 1e8ca0448b3b..a1bb008a2572 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -1,4 +1,4 @@ -import type { Client, DynamicSamplingContext, Span } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Scope, Span } from '@sentry/types'; import { DEFAULT_ENVIRONMENT } from '../constants'; import { getClient } from '../currentScopes'; @@ -51,6 +51,14 @@ export function getDynamicSamplingContextFromClient(trace_id: string, client: Cl return dsc; } +/** + * Get the dynamic sampling context for the currently active scopes. + */ +export function getDynamicSamplingContextFromScope(client: Client, scope: Scope): Partial { + const propagationContext = scope.getPropagationContext(); + return propagationContext.dsc || getDynamicSamplingContextFromClient(propagationContext.traceId, client); +} + /** * Creates a dynamic sampling context from a span (and client and scope) * @@ -64,8 +72,6 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly = Array | T>; -/** Flattens a multi-dimensional array */ +/** Flattens a multi-dimensional array + * + * @deprecated This function is deprecated and will be removed in the next major version. + */ export function flatten(input: NestedArray): T[] { const result: T[] = []; diff --git a/packages/core/src/utils-hoist/baggage.ts b/packages/core/src/utils-hoist/baggage.ts index 8cc2dfd68ef2..93c8c3918748 100644 --- a/packages/core/src/utils-hoist/baggage.ts +++ b/packages/core/src/utils-hoist/baggage.ts @@ -4,6 +4,9 @@ import { DEBUG_BUILD } from './debug-build'; import { isString } from './is'; import { logger } from './logger'; +/** + * @deprecated Use a `"baggage"` string directly + */ export const BAGGAGE_HEADER_NAME = 'baggage'; export const SENTRY_BAGGAGE_KEY_PREFIX = 'sentry-'; diff --git a/packages/core/src/utils-hoist/cache.ts b/packages/core/src/utils-hoist/cache.ts index 412970e77c76..376f8ef970cc 100644 --- a/packages/core/src/utils-hoist/cache.ts +++ b/packages/core/src/utils-hoist/cache.ts @@ -1,6 +1,8 @@ /** * Creates a cache that evicts keys in fifo order * @param size {Number} + * + * @deprecated This function is deprecated and will be removed in the next major version. */ export function makeFifoCache( size: number, diff --git a/packages/core/src/utils-hoist/debug-ids.ts b/packages/core/src/utils-hoist/debug-ids.ts index 4802b9356965..d407e6176e45 100644 --- a/packages/core/src/utils-hoist/debug-ids.ts +++ b/packages/core/src/utils-hoist/debug-ids.ts @@ -1,7 +1,12 @@ -import type { DebugImage, StackFrame, StackParser } from '@sentry/types'; +import type { DebugImage, StackParser } from '@sentry/types'; import { GLOBAL_OBJ } from './worldwide'; -const debugIdStackParserCache = new WeakMap>(); +type StackString = string; +type CachedResult = [string, string]; + +let parsedStackResults: Record | undefined; +let lastKeysCount: number | undefined; +let cachedFilenameDebugIds: Record | undefined; /** * Returns a map of filenames to debug identifiers. @@ -12,38 +17,46 @@ export function getFilenameToDebugIdMap(stackParser: StackParser): Record; - const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(stackParser); - if (cachedDebugIdStackFrameCache) { - debugIdStackFramesCache = cachedDebugIdStackFrameCache; - } else { - debugIdStackFramesCache = new Map(); - debugIdStackParserCache.set(stackParser, debugIdStackFramesCache); + const debugIdKeys = Object.keys(debugIdMap); + + // If the count of registered globals hasn't changed since the last call, we + // can just return the cached result. + if (cachedFilenameDebugIds && debugIdKeys.length === lastKeysCount) { + return cachedFilenameDebugIds; } + lastKeysCount = debugIdKeys.length; + // Build a map of filename -> debug_id. - return Object.keys(debugIdMap).reduce>((acc, debugIdStackTrace) => { - let parsedStack: StackFrame[]; + cachedFilenameDebugIds = debugIdKeys.reduce>((acc, stackKey) => { + if (!parsedStackResults) { + parsedStackResults = {}; + } + + const result = parsedStackResults[stackKey]; - const cachedParsedStack = debugIdStackFramesCache.get(debugIdStackTrace); - if (cachedParsedStack) { - parsedStack = cachedParsedStack; + if (result) { + acc[result[0]] = result[1]; } else { - parsedStack = stackParser(debugIdStackTrace); - debugIdStackFramesCache.set(debugIdStackTrace, parsedStack); - } + const parsedStack = stackParser(stackKey); - for (let i = parsedStack.length - 1; i >= 0; i--) { - const stackFrame = parsedStack[i]; - const file = stackFrame && stackFrame.filename; + for (let i = parsedStack.length - 1; i >= 0; i--) { + const stackFrame = parsedStack[i]; + const filename = stackFrame && stackFrame.filename; + const debugId = debugIdMap[stackKey]; - if (stackFrame && file) { - acc[file] = debugIdMap[debugIdStackTrace] as string; - break; + if (filename && debugId) { + acc[filename] = debugId; + parsedStackResults[stackKey] = [filename, debugId]; + break; + } } } + return acc; }, {}); + + return cachedFilenameDebugIds; } /** @@ -55,6 +68,10 @@ export function getDebugImagesForResources( ): DebugImage[] { const filenameDebugIdMap = getFilenameToDebugIdMap(stackParser); + if (!filenameDebugIdMap) { + return []; + } + const images: DebugImage[] = []; for (const path of resource_paths) { if (path && filenameDebugIdMap[path]) { diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index 1625ea6c0868..ce6be00849df 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -1,4 +1,5 @@ export { applyAggregateErrorsToEvent } from './aggregate-errors'; +// eslint-disable-next-line deprecation/deprecation export { flatten } from './array'; export { getBreadcrumbLogLevelFromHttpStatusCode } from './breadcrumb-log-level'; export { getComponentName, getDomElement, getLocationHref, htmlTreeAsString } from './browser'; @@ -35,11 +36,13 @@ export { } from './is'; export { isBrowser } from './isBrowser'; export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods } from './logger'; +// eslint-disable-next-line deprecation/deprecation export { memoBuilder } from './memo'; export { addContextToFrame, addExceptionMechanism, addExceptionTypeValue, + // eslint-disable-next-line deprecation/deprecation arrayify, checkOrSetAlreadyCaught, getEventDescription, @@ -57,6 +60,7 @@ export { getOriginalFunction, markFunctionWrapped, objectify, + // eslint-disable-next-line deprecation/deprecation urlEncode, } from './object'; export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './path'; @@ -67,9 +71,11 @@ export type { PromiseBuffer } from './promisebuffer'; export { DEFAULT_USER_INCLUDES, addNormalizedRequestDataToEvent, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, // eslint-disable-next-line deprecation/deprecation extractPathForTransaction, + // eslint-disable-next-line deprecation/deprecation extractRequestData, winterCGHeadersToDict, winterCGRequestToRequestData, @@ -83,6 +89,7 @@ export type { TransactionNamingScheme, } from './requestdata'; +// eslint-disable-next-line deprecation/deprecation export { severityLevelFromString, validSeverityLevels } from './severity'; export { UNKNOWN_FUNCTION, @@ -142,6 +149,7 @@ export { } from './ratelimit'; export type { RateLimits } from './ratelimit'; export { + // eslint-disable-next-line deprecation/deprecation BAGGAGE_HEADER_NAME, MAX_BAGGAGE_STRING_LENGTH, SENTRY_BAGGAGE_KEY_PREFIX, @@ -151,7 +159,9 @@ export { parseBaggageHeader, } from './baggage'; +// eslint-disable-next-line deprecation/deprecation export { getNumberOfUrlSegments, getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from './url'; +// eslint-disable-next-line deprecation/deprecation export { makeFifoCache } from './cache'; export { eventFromMessage, eventFromUnknownInput, exceptionFromError, parseStackFrames } from './eventbuilder'; export { callFrameToStackFrame, watchdogTimer } from './anr'; diff --git a/packages/core/src/utils-hoist/memo.ts b/packages/core/src/utils-hoist/memo.ts index d76f60579bc4..f7303bd44ece 100644 --- a/packages/core/src/utils-hoist/memo.ts +++ b/packages/core/src/utils-hoist/memo.ts @@ -10,7 +10,10 @@ export type MemoFunc = [ /** * Helper to decycle json objects + * + * @deprecated This function is deprecated and will be removed in the next major version. */ +// TODO(v9): Move this function into normalize() directly export function memoBuilder(): MemoFunc { const hasWeakSet = typeof WeakSet === 'function'; const inner: any = hasWeakSet ? new WeakSet() : []; diff --git a/packages/core/src/utils-hoist/misc.ts b/packages/core/src/utils-hoist/misc.ts index ee48a2d60c2d..0f02b5ba14de 100644 --- a/packages/core/src/utils-hoist/misc.ts +++ b/packages/core/src/utils-hoist/misc.ts @@ -232,6 +232,8 @@ export function checkOrSetAlreadyCaught(exception: unknown): boolean { * * @param maybeArray Input to turn into an array, if necessary * @returns The input, if already an array, or an array with the input as the only element, if not + * + * @deprecated This function has been deprecated and will not be replaced. */ export function arrayify(maybeArray: T | T[]): T[] { return Array.isArray(maybeArray) ? maybeArray : [maybeArray]; diff --git a/packages/core/src/utils-hoist/normalize.ts b/packages/core/src/utils-hoist/normalize.ts index e88b1edd8513..fe108fdbdd0e 100644 --- a/packages/core/src/utils-hoist/normalize.ts +++ b/packages/core/src/utils-hoist/normalize.ts @@ -74,6 +74,7 @@ function visit( value: unknown, depth: number = +Infinity, maxProperties: number = +Infinity, + // eslint-disable-next-line deprecation/deprecation memo: MemoFunc = memoBuilder(), ): Primitive | ObjOrArray { const [memoize, unmemoize] = memo; diff --git a/packages/core/src/utils-hoist/object.ts b/packages/core/src/utils-hoist/object.ts index 13ddff35664b..d3e785f7639d 100644 --- a/packages/core/src/utils-hoist/object.ts +++ b/packages/core/src/utils-hoist/object.ts @@ -90,6 +90,8 @@ export function getOriginalFunction(func: WrappedFunction): WrappedFunction | un * * @param object An object that contains serializable values * @returns string Encoded + * + * @deprecated This function is deprecated and will be removed in the next major version of the SDK. */ export function urlEncode(object: { [key: string]: any }): string { return Object.keys(object) diff --git a/packages/core/src/utils-hoist/requestdata.ts b/packages/core/src/utils-hoist/requestdata.ts index 5a40c1fa5945..975b993f69c7 100644 --- a/packages/core/src/utils-hoist/requestdata.ts +++ b/packages/core/src/utils-hoist/requestdata.ts @@ -134,6 +134,8 @@ function extractUserData( * DEFAULT_REQUEST_INCLUDES if not provided. * @param options.deps Injected, platform-specific dependencies * @returns An object containing normalized request data + * + * @deprecated Instead manually normalize the request data into a format that fits `addNormalizedRequestDataToEvent`. */ export function extractRequestData( req: PolymorphicRequest, @@ -318,6 +320,8 @@ export function addNormalizedRequestDataToEvent( * @param options.include Flags to control what data is included * @param options.deps Injected platform-specific dependencies * @returns The mutated `Event` object + * + * @deprecated Use `addNormalizedRequestDataToEvent` instead. */ export function addRequestDataToEvent( event: Event, @@ -335,6 +339,7 @@ export function addRequestDataToEvent( includeRequest.push('ip'); } + // eslint-disable-next-line deprecation/deprecation const extractedRequestData = extractRequestData(req, { include: includeRequest }); event.request = { diff --git a/packages/core/src/utils-hoist/severity.ts b/packages/core/src/utils-hoist/severity.ts index c19c047c90bf..f5217b8b87c9 100644 --- a/packages/core/src/utils-hoist/severity.ts +++ b/packages/core/src/utils-hoist/severity.ts @@ -1,15 +1,8 @@ import type { SeverityLevel } from '@sentry/types'; -// Note: Ideally the `SeverityLevel` type would be derived from `validSeverityLevels`, but that would mean either -// -// a) moving `validSeverityLevels` to `@sentry/types`, -// b) moving the`SeverityLevel` type here, or -// c) importing `validSeverityLevels` from here into `@sentry/types`. -// -// Option A would make `@sentry/types` a runtime dependency of `@sentry/core` (not good), and options B and C would -// create a circular dependency between `@sentry/types` and `@sentry/core` (also not good). So a TODO accompanying the -// type, reminding anyone who changes it to change this list also, will have to do. - +/** + * @deprecated This variable has been deprecated and will be removed in the next major version. + */ export const validSeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug']; /** @@ -19,5 +12,7 @@ export const validSeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', * @returns The `SeverityLevel` corresponding to the given string, or 'log' if the string isn't a valid level. */ export function severityLevelFromString(level: SeverityLevel | string): SeverityLevel { - return (level === 'warn' ? 'warning' : validSeverityLevels.includes(level) ? level : 'log') as SeverityLevel; + return ( + level === 'warn' ? 'warning' : ['fatal', 'error', 'warning', 'log', 'info', 'debug'].includes(level) ? level : 'log' + ) as SeverityLevel; } diff --git a/packages/core/src/utils-hoist/tracing.ts b/packages/core/src/utils-hoist/tracing.ts index 69a18f1a4c38..04e041407f33 100644 --- a/packages/core/src/utils-hoist/tracing.ts +++ b/packages/core/src/utils-hoist/tracing.ts @@ -2,6 +2,7 @@ import type { PropagationContext, TraceparentData } from '@sentry/types'; import { baggageHeaderToDynamicSamplingContext } from './baggage'; import { uuid4 } from './misc'; +import { generatePropagationContext } from './propagationContext'; // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp is used for readability here export const TRACEPARENT_REGEXP = new RegExp( @@ -54,22 +55,21 @@ export function propagationContextFromHeaders( const traceparentData = extractTraceparentData(sentryTrace); const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggage); - const { traceId, parentSpanId, parentSampled } = traceparentData || {}; - - if (!traceparentData) { - return { - traceId: traceId || uuid4(), - spanId: uuid4().substring(16), - }; - } else { - return { - traceId: traceId || uuid4(), - parentSpanId: parentSpanId || uuid4().substring(16), - spanId: uuid4().substring(16), - sampled: parentSampled, - dsc: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it - }; + if (!traceparentData || !traceparentData.traceId) { + return generatePropagationContext(); } + + const { traceId, parentSpanId, parentSampled } = traceparentData; + + const virtualSpanId = uuid4().substring(16); + + return { + traceId, + parentSpanId, + spanId: virtualSpanId, + sampled: parentSampled, + dsc: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it + }; } /** diff --git a/packages/core/src/utils-hoist/url.ts b/packages/core/src/utils-hoist/url.ts index e324f41f82a3..44dc669da93a 100644 --- a/packages/core/src/utils-hoist/url.ts +++ b/packages/core/src/utils-hoist/url.ts @@ -50,7 +50,10 @@ export function stripUrlQueryAndFragment(urlPath: string): string { /** * Returns number of URL segments of a passed string URL. + * + * @deprecated This function will be removed in the next major version. */ +// TODO(v9): Hoist this function into the places where we use it. (as it stands only react router v6 instrumentation) export function getNumberOfUrlSegments(url: string): number { // split at '/' or at '\/' to split regex urls correctly return url.split(/\\?\//).filter(s => s.length > 0 && s !== ',').length; diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index bf23a0e3ee79..14358d4e3101 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -129,23 +129,26 @@ export function prepareEvent( } /** - * Enhances event using the client configuration. - * It takes care of all "static" values like environment, release and `dist`, - * as well as truncating overly long values. + * Enhances event using the client configuration. + * It takes care of all "static" values like environment, release and `dist`, + * as well as truncating overly long values. + * + * Only exported for tests. + * * @param event event instance to be enhanced */ -function applyClientOptions(event: Event, options: ClientOptions): void { +export function applyClientOptions(event: Event, options: ClientOptions): void { const { environment, release, dist, maxValueLength = 250 } = options; - if (!('environment' in event)) { - event.environment = 'environment' in options ? environment : DEFAULT_ENVIRONMENT; - } + // empty strings do not make sense for environment, release, and dist + // so we handle them the same as if they were not provided + event.environment = event.environment || environment || DEFAULT_ENVIRONMENT; - if (event.release === undefined && release !== undefined) { + if (!event.release && release) { event.release = release; } - if (event.dist === undefined && dist !== undefined) { + if (!event.dist && dist) { event.dist = dist; } @@ -176,7 +179,7 @@ export function applyDebugIds(event: Event, stackParser: StackParser): void { event!.exception!.values!.forEach(exception => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion exception.stacktrace!.frames!.forEach(frame => { - if (frame.filename) { + if (filenameDebugIdMap && frame.filename) { frame.debug_id = filenameDebugIdMap[frame.filename]; } }); diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 5f0c443919a3..d9232b1f48bc 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -17,6 +17,7 @@ import type { MetricType } from '../metrics/types'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { SentrySpan } from '../tracing/sentrySpan'; import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; +import { consoleSandbox } from '../utils-hoist/logger'; import { addNonEnumerableProperty, dropUndefinedKeys } from '../utils-hoist/object'; import { timestampInSeconds } from '../utils-hoist/time'; import { generateSentryTraceHeader } from '../utils-hoist/tracing'; @@ -26,6 +27,9 @@ import { _getSpanForScope } from './spanOnScope'; export const TRACE_FLAG_NONE = 0x0; export const TRACE_FLAG_SAMPLED = 0x1; +// todo(v9): Remove this once we've stopped dropping spans via `beforeSendSpan` +let hasShownSpanDropWarning = false; + /** * Convert a span to a trace context, which can be sent as the `trace` context in an event. * By default, this will only include trace_id, span_id & parent_span_id. @@ -280,3 +284,20 @@ export function updateMetricSummaryOnActiveSpan( updateMetricSummaryOnSpan(span, metricType, sanitizedName, value, unit, tags, bucketKey); } } + +/** + * Logs a warning once if `beforeSendSpan` is used to drop spans. + * + * todo(v9): Remove this once we've stopped dropping spans via `beforeSendSpan`. + */ +export function showSpanDropWarning(): void { + if (!hasShownSpanDropWarning) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.', + ); + }); + hasShownSpanDropWarning = true; + } +} diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index 04727510e07c..4892e558661a 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -1,13 +1,13 @@ -import type { SerializedTraceData } from '@sentry/types'; +import type { Scope, SerializedTraceData, Span } from '@sentry/types'; import { getAsyncContextStrategy } from '../asyncContext'; import { getMainCarrier } from '../carrier'; import { getClient, getCurrentScope } from '../currentScopes'; import { isEnabled } from '../exports'; -import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from '../tracing'; +import { getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan } from '../tracing'; import { dynamicSamplingContextToSentryBaggageHeader } from '../utils-hoist/baggage'; import { logger } from '../utils-hoist/logger'; import { TRACEPARENT_REGEXP, generateSentryTraceHeader } from '../utils-hoist/tracing'; -import { getActiveSpan, getRootSpan, spanToTraceHeader } from './spanUtils'; +import { getActiveSpan, spanToTraceHeader } from './spanUtils'; /** * Extracts trace propagation data from the current span or from the client's scope (via transaction or propagation @@ -20,35 +20,23 @@ import { getActiveSpan, getRootSpan, spanToTraceHeader } from './spanUtils'; * @returns an object with the tracing data values. The object keys are the name of the tracing key to be used as header * or meta tag name. */ -export function getTraceData(): SerializedTraceData { - if (!isEnabled()) { +export function getTraceData(options: { span?: Span } = {}): SerializedTraceData { + const client = getClient(); + if (!isEnabled() || !client) { return {}; } const carrier = getMainCarrier(); const acs = getAsyncContextStrategy(carrier); if (acs.getTraceData) { - return acs.getTraceData(); + return acs.getTraceData(options); } - const client = getClient(); const scope = getCurrentScope(); - const span = getActiveSpan(); - - const { dsc, sampled, traceId } = scope.getPropagationContext(); - const rootSpan = span && getRootSpan(span); - - const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); - - const dynamicSamplingContext = rootSpan - ? getDynamicSamplingContextFromSpan(rootSpan) - : dsc - ? dsc - : client - ? getDynamicSamplingContextFromClient(traceId, client) - : undefined; - - const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + const span = options.span || getActiveSpan(); + const sentryTrace = span ? spanToTraceHeader(span) : scopeToTraceHeader(scope); + const dsc = span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromScope(client, scope); + const baggage = dynamicSamplingContextToSentryBaggageHeader(dsc); const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace); if (!isValidSentryTraceHeader) { @@ -56,35 +44,16 @@ export function getTraceData(): SerializedTraceData { return {}; } - const validBaggage = isValidBaggageString(baggage); - if (!validBaggage) { - logger.warn('Invalid baggage data. Not returning "baggage" value'); - } - return { 'sentry-trace': sentryTrace, - ...(validBaggage && { baggage }), + baggage, }; } /** - * Tests string against baggage spec as defined in: - * - * - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition - * - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 - * - * exported for testing + * Get a sentry-trace header value for the given scope. */ -export function isValidBaggageString(baggage?: string): boolean { - if (!baggage || !baggage.length) { - return false; - } - const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+"; - const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+'; - const spaces = '\\s*'; - // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input - const baggageRegex = new RegExp( - `^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`, - ); - return baggageRegex.test(baggage); +function scopeToTraceHeader(scope: Scope): string { + const { traceId, sampled, spanId } = scope.getPropagationContext(); + return generateSentryTraceHeader(traceId, spanId, sampled); } diff --git a/packages/core/test/lib/api.test.ts b/packages/core/test/lib/api.test.ts index 2c581937cc42..89a8ad3cf20f 100644 --- a/packages/core/test/lib/api.test.ts +++ b/packages/core/test/lib/api.test.ts @@ -18,7 +18,7 @@ describe('API', () => { dsnPublicComponents, undefined, undefined, - 'https://sentry.io:1234/subpath/api/123/envelope/?sentry_key=abc&sentry_version=7', + 'https://sentry.io:1234/subpath/api/123/envelope/?sentry_version=7&sentry_key=abc', ], ['uses `tunnel` value when called with `tunnel` option', dsnPublicComponents, tunnel, undefined, tunnel], [ @@ -33,7 +33,7 @@ describe('API', () => { dsnPublicComponents, undefined, sdkInfo, - 'https://sentry.io:1234/subpath/api/123/envelope/?sentry_key=abc&sentry_version=7&sentry_client=sentry.javascript.browser%2F12.31.12', + 'https://sentry.io:1234/subpath/api/123/envelope/?sentry_version=7&sentry_key=abc&sentry_client=sentry.javascript.browser%2F12.31.12', ], ])( '%s', diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/baseclient.test.ts similarity index 96% rename from packages/core/test/lib/base.test.ts rename to packages/core/test/lib/baseclient.test.ts index 296a496d2d40..4e2bcdb334ef 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/baseclient.test.ts @@ -77,6 +77,56 @@ describe('BaseClient', () => { }); }); + describe('constructor() / warnings', () => { + test('does not warn for defaults', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + new TestClient(options); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + consoleWarnSpy.mockRestore(); + }); + + describe.each(['tracesSampleRate', 'tracesSampler', 'enableTracing'])('%s', key => { + it('warns when set to undefined', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: undefined }); + new TestClient(options); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toBeCalledWith( + `[Sentry] Deprecation warning: \`${key}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`, + ); + consoleWarnSpy.mockRestore(); + }); + + it('warns when set to null', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: null }); + new TestClient(options); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toBeCalledWith( + `[Sentry] Deprecation warning: \`${key}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`, + ); + consoleWarnSpy.mockRestore(); + }); + + it('does not warn when set to 0', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: 0 }); + new TestClient(options); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + consoleWarnSpy.mockRestore(); + }); + }); + }); + describe('getOptions()', () => { test('returns the options', () => { expect.assertions(1); @@ -552,7 +602,7 @@ describe('BaseClient', () => { ); }); - test('allows for environment to be explicitly set to falsy value', () => { + test('uses default environment when set to falsy value', () => { expect.assertions(1); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, environment: undefined }); @@ -563,7 +613,7 @@ describe('BaseClient', () => { expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ - environment: undefined, + environment: 'production', event_id: '42', message: 'message', timestamp: 2020, @@ -1122,6 +1172,8 @@ describe('BaseClient', () => { }); test('calls `beforeSendSpan` and discards the span', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const beforeSendSpan = jest.fn(() => null); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); const client = new TestClient(options); @@ -1150,6 +1202,12 @@ describe('BaseClient', () => { const capturedEvent = TestClient.instance!.event!; expect(capturedEvent.spans).toHaveLength(0); expect(client['_outcomes']).toEqual({ 'before_send:span': 2 }); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toBeCalledWith( + '[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.', + ); + consoleWarnSpy.mockRestore(); }); test('calls `beforeSend` and logs info about invalid return value', () => { diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index a905d948475a..0322afb7e7a8 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -12,6 +12,7 @@ import { GLOBAL_OBJ, createStackParser, getGlobalScope, getIsolationScope } from import { Scope } from '../../src/scope'; import { + applyClientOptions, applyDebugIds, applyDebugMeta, parseEventHintOrCaptureContext, @@ -518,3 +519,136 @@ describe('prepareEvent', () => { }); }); }); + +describe('applyClientOptions', () => { + it('works with defaults', () => { + const event: Event = {}; + const options = {} as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'production', + }); + + // These should not be set at all on the event + expect('release' in event).toBe(false); + expect('dist' in event).toBe(false); + }); + + it('works with event data and no options', () => { + const event: Event = { + environment: 'blub', + release: 'blab', + dist: 'blib', + }; + const options = {} as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'blub', + release: 'blab', + dist: 'blib', + }); + }); + + it('event data has precedence over options', () => { + const event: Event = { + environment: 'blub', + release: 'blab', + dist: 'blib', + }; + const options = { + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + } as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'blub', + release: 'blab', + dist: 'blib', + }); + }); + + it('option data is used if no event data exists', () => { + const event: Event = {}; + const options = { + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + } as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + }); + }); + + it('option data is ignored if empty string', () => { + const event: Event = {}; + const options = { + environment: '', + release: '', + dist: '', + } as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'production', + }); + + // These should not be set at all on the event + expect('release' in event).toBe(false); + expect('dist' in event).toBe(false); + }); + + it('option data is used if event data is undefined', () => { + const event: Event = { + environment: undefined, + release: undefined, + dist: undefined, + }; + const options = { + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + } as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + }); + }); + + it('option data is used if event data is empty string', () => { + const event: Event = { + environment: '', + release: '', + dist: '', + }; + const options = { + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + } as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + }); + }); +}); diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index 447e42328a2e..9a1c9f69255c 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -161,6 +161,8 @@ describe('SentrySpan', () => { }); test('does not send the span if `beforeSendSpan` drops the span', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const beforeSendSpan = jest.fn(() => null); const client = new TestClient( getDefaultTestClientOptions({ @@ -185,6 +187,12 @@ describe('SentrySpan', () => { expect(mockSend).not.toHaveBeenCalled(); expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'span'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toBeCalledWith( + '[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.', + ); + consoleWarnSpy.mockRestore(); }); }); diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index aa6d2497dd54..d73ab091d8eb 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -1,231 +1,282 @@ -import { SentrySpan, getTraceData } from '../../../src/'; -import * as SentryCoreCurrentScopes from '../../../src/currentScopes'; -import * as SentryCoreExports from '../../../src/exports'; -import * as SentryCoreTracing from '../../../src/tracing'; -import * as SentryCoreSpanUtils from '../../../src/utils/spanUtils'; - -import { isValidBaggageString } from '../../../src/utils/traceData'; - -const TRACE_FLAG_SAMPLED = 1; - -const mockedSpan = new SentrySpan({ - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', - sampled: true, -}); +import type { Client, Span } from '@sentry/types'; +import { + SentrySpan, + getCurrentScope, + getGlobalScope, + getIsolationScope, + getMainCarrier, + getTraceData, + setAsyncContextStrategy, + setCurrentClient, + withActiveSpan, +} from '../../../src/'; +import { getAsyncContextStrategy } from '../../../src/asyncContext'; +import { freezeDscOnSpan } from '../../../src/tracing/dynamicSamplingContext'; + +import type { TestClientOptions } from '../../mocks/client'; +import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; + +const dsn = 'https://123@sentry.io/42'; + +const SCOPE_TRACE_ID = '12345678901234567890123456789012'; +const SCOPE_SPAN_ID = '1234567890123456'; + +function setupClient(opts?: Partial): Client { + getCurrentScope().setPropagationContext({ + traceId: SCOPE_TRACE_ID, + spanId: SCOPE_SPAN_ID, + }); -const mockedClient = {} as any; + const options = getDefaultTestClientOptions({ + dsn, + tracesSampleRate: 1, + ...opts, + }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); -const mockedScope = { - getPropagationContext: () => ({ - traceId: '123', - }), -} as any; + return client; +} describe('getTraceData', () => { beforeEach(() => { - jest.spyOn(SentryCoreExports, 'isEnabled').mockReturnValue(true); + setAsyncContextStrategy(undefined); + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + getCurrentScope().setClient(undefined); }); afterEach(() => { jest.clearAllMocks(); }); - it('returns the tracing data from the span, if a span is available', () => { - { - jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ - environment: 'production', - }); - jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => mockedSpan); - jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope); + it('uses the ACS implementation, if available', () => { + setupClient(); + + const carrier = getMainCarrier(); + + const customFn = jest.fn((options?: { span?: Span }) => { + expect(options).toEqual({ span: undefined }); + return { + 'sentry-trace': 'abc', + baggage: 'xyz', + }; + }) as typeof getTraceData; + + const acs = { + ...getAsyncContextStrategy(carrier), + getTraceData: customFn, + }; + setAsyncContextStrategy(acs); + + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, + }); + withActiveSpan(span, () => { const data = getTraceData(); expect(data).toEqual({ - 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', - baggage: 'sentry-environment=production', + 'sentry-trace': 'abc', + baggage: 'xyz', }); - } + }); }); - it('returns propagationContext DSC data if no span is available', () => { - jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => undefined); - jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce( - () => - ({ - getPropagationContext: () => ({ - traceId: '12345678901234567890123456789012', - sampled: true, - spanId: '1234567890123456', - dsc: { - environment: 'staging', - public_key: 'key', - trace_id: '12345678901234567890123456789012', - }, - }), - }) as any, - ); - jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => mockedClient); + it('passes span to ACS implementation, if available', () => { + setupClient(); - const traceData = getTraceData(); + const carrier = getMainCarrier(); - expect(traceData).toEqual({ - 'sentry-trace': expect.stringMatching(/12345678901234567890123456789012-(.{16})-1/), - baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012', + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, + }); + + const customFn = jest.fn((options?: { span?: Span }) => { + expect(options).toEqual({ span }); + return { + 'sentry-trace': 'abc', + baggage: 'xyz', + }; + }) as typeof getTraceData; + + const acs = { + ...getAsyncContextStrategy(carrier), + getTraceData: customFn, + }; + setAsyncContextStrategy(acs); + + const data = getTraceData({ span }); + + expect(data).toEqual({ + 'sentry-trace': 'abc', + baggage: 'xyz', }); }); - it('returns only the `sentry-trace` value if no DSC is available', () => { - jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ - trace_id: '', - public_key: undefined, - }); - - // @ts-expect-error - we don't need to provide all the properties - jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({ - isRecording: () => true, - spanContext: () => { - return { - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', - traceFlags: TRACE_FLAG_SAMPLED, - }; - }, - })); + it('returns the tracing data from the span, if a span is available', () => { + setupClient(); - jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope); - jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => mockedClient); + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, + }); - const traceData = getTraceData(); + withActiveSpan(span, () => { + const data = getTraceData(); - expect(traceData).toEqual({ + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: + 'sentry-environment=production,sentry-public_key=123,sentry-trace_id=12345678901234567890123456789012,sentry-sampled=true', + }); + }); + }); + + it('allows to pass a span directly', () => { + setupClient(); + + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, + }); + + const data = getTraceData({ span }); + + expect(data).toEqual({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: + 'sentry-environment=production,sentry-public_key=123,sentry-trace_id=12345678901234567890123456789012,sentry-sampled=true', }); }); - it('returns only the `sentry-trace` tag if no DSC is available without a client', () => { - jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ - trace_id: '', - public_key: undefined, - }); - - // @ts-expect-error - we don't need to provide all the properties - jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({ - isRecording: () => true, - spanContext: () => { - return { - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', - traceFlags: TRACE_FLAG_SAMPLED, - }; + it('returns propagationContext DSC data if no span is available', () => { + setupClient(); + + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampled: true, + spanId: '1234567890123456', + dsc: { + environment: 'staging', + public_key: 'key', + trace_id: '12345678901234567890123456789012', }, - })); - jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope); - jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => undefined); + }); const traceData = getTraceData(); expect(traceData).toEqual({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012', }); - expect('baggage' in traceData).toBe(false); }); - it('returns an empty object if the `sentry-trace` value is invalid', () => { - // @ts-expect-error - we don't need to provide all the properties - jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({ - isRecording: () => true, - spanContext: () => { - return { - traceId: '1234567890123456789012345678901+', - spanId: '1234567890123456', - traceFlags: TRACE_FLAG_SAMPLED, - }; - }, - })); + it('returns frozen DSC from SentrySpan if available', () => { + setupClient(); - const traceData = getTraceData(); + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, + }); - expect(traceData).toEqual({}); + freezeDscOnSpan(span, { + environment: 'test-dev', + public_key: '456', + trace_id: '12345678901234567890123456789088', + }); + + withActiveSpan(span, () => { + const data = getTraceData(); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=test-dev,sentry-public_key=456,sentry-trace_id=12345678901234567890123456789088', + }); + }); }); - it('returns an empty object if the SDK is disabled', () => { - jest.spyOn(SentryCoreExports, 'isEnabled').mockReturnValueOnce(false); + it('works with an OTEL span with frozen DSC in traceState', () => { + setupClient(); + const traceId = '12345678901234567890123456789099'; + const spanId = '1234567890123499'; + + const span = new SentrySpan({ + traceId, + spanId, + sampled: true, + }); + + span.spanContext = () => { + const traceState = { + set: () => traceState, + unset: () => traceState, + get: (key: string) => { + if (key === 'sentry.dsc') { + return 'sentry-environment=test-dev,sentry-public_key=456,sentry-trace_id=12345678901234567890123456789088'; + } + return undefined; + }, + serialize: () => '', + }; + + return { + traceId, + spanId, + sampled: true, + traceFlags: 1, + traceState, + }; + }; + + withActiveSpan(span, () => { + const data = getTraceData(); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789099-1234567890123499-1', + baggage: 'sentry-environment=test-dev,sentry-public_key=456,sentry-trace_id=12345678901234567890123456789088', + }); + }); + }); + + it('returns empty object without a client', () => { const traceData = getTraceData(); expect(traceData).toEqual({}); }); -}); -describe('isValidBaggageString', () => { - it.each([ - 'sentry-environment=production', - 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=abc', - // @ is allowed in values - 'sentry-release=project@1.0.0', - // spaces are allowed around the delimiters - 'sentry-environment=staging , sentry-public_key=key ,sentry-release=myproject@1.0.0', - 'sentry-environment=staging , thirdparty=value ,sentry-release=myproject@1.0.0', - // these characters are explicitly allowed for keys in the baggage spec: - "!#$%&'*+-.^_`|~1234567890abcxyzABCXYZ=true", - // special characters in values are fine (except for ",;\ - see other test) - 'key=(value)', - 'key=[{(value)}]', - 'key=some$value', - 'key=more#value', - 'key=max&value', - 'key=max:value', - 'key=x=value', - ])('returns true if the baggage string is valid (%s)', baggageString => { - expect(isValidBaggageString(baggageString)).toBe(true); - }); + it('returns an empty object if the `sentry-trace` value is invalid', () => { + // Invalid traceID + const traceId = '1234567890123456789012345678901+'; + const spanId = '1234567890123499'; + + const span = new SentrySpan({ + traceId, + spanId, + sampled: true, + }); - it.each([ - // baggage spec doesn't permit leading spaces - ' sentry-environment=production,sentry-publickey=key,sentry-trace_id=abc', - // no spaces in keys or values - 'sentry-public key=key', - 'sentry-publickey=my key', - // no delimiters ("(),/:;<=>?@[\]{}") in keys - 'asdf(x=value', - 'asdf)x=value', - 'asdf,x=value', - 'asdf/x=value', - 'asdf:x=value', - 'asdf;x=value', - 'asdfx=value', - 'asdf?x=value', - 'asdf@x=value', - 'asdf[x=value', - 'asdf]x=value', - 'asdf\\x=value', - 'asdf{x=value', - 'asdf}x=value', - // no ,;\" in values - 'key=va,lue', - 'key=va;lue', - 'key=va\\lue', - 'key=va"lue"', - // baggage headers can have properties but we currently don't support them - 'sentry-environment=production;prop1=foo;prop2=bar,nextkey=value', - // no fishy stuff - 'absolutely not a valid baggage string', - 'val"/>', - 'something"/>', - '', - '/>', - '" onblur="alert("xss")', - ])('returns false if the baggage string is invalid (%s)', baggageString => { - expect(isValidBaggageString(baggageString)).toBe(false); + withActiveSpan(span, () => { + const data = getTraceData(); + expect(data).toEqual({}); + }); }); - it('returns false if the baggage string is empty', () => { - expect(isValidBaggageString('')).toBe(false); - }); + it('returns an empty object if the SDK is disabled', () => { + setupClient({ dsn: undefined }); + + const traceData = getTraceData(); - it('returns false if the baggage string is empty', () => { - expect(isValidBaggageString(undefined)).toBe(false); + expect(traceData).toEqual({}); }); }); diff --git a/packages/core/test/utils-hoist/array.test.ts b/packages/core/test/utils-hoist/array.test.ts index fb788d48cea9..3716a6d190b1 100644 --- a/packages/core/test/utils-hoist/array.test.ts +++ b/packages/core/test/utils-hoist/array.test.ts @@ -1,16 +1,19 @@ import type { NestedArray } from '../../src/utils-hoist/array'; +// eslint-disable-next-line deprecation/deprecation import { flatten } from '../../src/utils-hoist/array'; describe('flatten', () => { it('should return the same array when input is a flat array', () => { const input = [1, 2, 3, 4]; const expected = [1, 2, 3, 4]; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); it('should flatten a nested array of numbers', () => { const input = [[1, 2, [3]], 4]; const expected = [1, 2, 3, 4]; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); @@ -20,6 +23,7 @@ describe('flatten', () => { ['How', 'Are', 'You'], ]; const expected = ['Hello', 'World', 'How', 'Are', 'You']; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); @@ -29,30 +33,35 @@ describe('flatten', () => { [{ a: 3 }, { b: 4 }], ]; const expected = [{ a: 1 }, { b: 2 }, { a: 3 }, { b: 4 }]; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); it('should flatten a mixed type array', () => { const input: NestedArray = [['a', { b: 2 }, 'c'], 'd']; const expected = ['a', { b: 2 }, 'c', 'd']; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); it('should flatten a deeply nested array', () => { const input = [1, [2, [3, [4, [5]]]]]; const expected = [1, 2, 3, 4, 5]; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); it('should return an empty array when input is empty', () => { const input: any[] = []; const expected: any[] = []; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); it('should return the same array when input is a flat array', () => { const input = [1, 'a', { b: 2 }, 'c', 3]; const expected = [1, 'a', { b: 2 }, 'c', 3]; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); }); diff --git a/packages/core/test/utils-hoist/misc.test.ts b/packages/core/test/utils-hoist/misc.test.ts index b2c37758e79c..679f0173ffca 100644 --- a/packages/core/test/utils-hoist/misc.test.ts +++ b/packages/core/test/utils-hoist/misc.test.ts @@ -366,16 +366,24 @@ describe('uuid4 generation', () => { describe('arrayify()', () => { it('returns arrays untouched', () => { + // eslint-disable-next-line deprecation/deprecation expect(arrayify([])).toEqual([]); + // eslint-disable-next-line deprecation/deprecation expect(arrayify(['dogs', 'are', 'great'])).toEqual(['dogs', 'are', 'great']); }); it('wraps non-arrays with an array', () => { + // eslint-disable-next-line deprecation/deprecation expect(arrayify(1231)).toEqual([1231]); + // eslint-disable-next-line deprecation/deprecation expect(arrayify('dogs are great')).toEqual(['dogs are great']); + // eslint-disable-next-line deprecation/deprecation expect(arrayify(true)).toEqual([true]); + // eslint-disable-next-line deprecation/deprecation expect(arrayify({})).toEqual([{}]); + // eslint-disable-next-line deprecation/deprecation expect(arrayify(null)).toEqual([null]); + // eslint-disable-next-line deprecation/deprecation expect(arrayify(undefined)).toEqual([undefined]); }); }); diff --git a/packages/core/test/utils-hoist/object.test.ts b/packages/core/test/utils-hoist/object.test.ts index 7e39c463336c..2fadb530bdf2 100644 --- a/packages/core/test/utils-hoist/object.test.ts +++ b/packages/core/test/utils-hoist/object.test.ts @@ -130,14 +130,17 @@ describe('fill()', () => { describe('urlEncode()', () => { test('returns empty string for empty object input', () => { + // eslint-disable-next-line deprecation/deprecation expect(urlEncode({})).toEqual(''); }); test('returns single key/value pair joined with = sign', () => { + // eslint-disable-next-line deprecation/deprecation expect(urlEncode({ foo: 'bar' })).toEqual('foo=bar'); }); test('returns multiple key/value pairs joined together with & sign', () => { + // eslint-disable-next-line deprecation/deprecation expect(urlEncode({ foo: 'bar', pickle: 'rick', morty: '4 2' })).toEqual('foo=bar&pickle=rick&morty=4%202'); }); }); diff --git a/packages/core/test/utils-hoist/requestdata.test.ts b/packages/core/test/utils-hoist/requestdata.test.ts index 0b1f198ebddc..3df3ee7e84eb 100644 --- a/packages/core/test/utils-hoist/requestdata.test.ts +++ b/packages/core/test/utils-hoist/requestdata.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable deprecation/deprecation */ import type * as net from 'net'; import { addRequestDataToEvent, extractPathForTransaction, extractRequestData } from '@sentry/core'; import type { Event, PolymorphicRequest, TransactionSource, User } from '@sentry/types'; diff --git a/packages/core/test/utils-hoist/severity.test.ts b/packages/core/test/utils-hoist/severity.test.ts index 30e3dbb90e97..65388428b65c 100644 --- a/packages/core/test/utils-hoist/severity.test.ts +++ b/packages/core/test/utils-hoist/severity.test.ts @@ -1,4 +1,4 @@ -import { severityLevelFromString, validSeverityLevels } from '../../src/utils-hoist/severity'; +import { severityLevelFromString } from '../../src/utils-hoist/severity'; describe('severityLevelFromString()', () => { test("converts 'warn' to 'warning'", () => { @@ -10,7 +10,7 @@ describe('severityLevelFromString()', () => { }); test('acts as a pass-through for valid level strings', () => { - for (const level of validSeverityLevels) { + for (const level of ['fatal', 'error', 'warning', 'log', 'info', 'debug']) { expect(severityLevelFromString(level)).toBe(level); } }); diff --git a/packages/core/test/utils-hoist/url.test.ts b/packages/core/test/utils-hoist/url.test.ts index fd8b861516ab..cd793e1d3d0c 100644 --- a/packages/core/test/utils-hoist/url.test.ts +++ b/packages/core/test/utils-hoist/url.test.ts @@ -33,6 +33,7 @@ describe('getNumberOfUrlSegments', () => { ['multi param parameterized path', '/stores/:storeId/products/:productId', 4], ['regex path', String(/\/api\/post[0-9]/), 2], ])('%s', (_: string, input, output) => { + // eslint-disable-next-line deprecation/deprecation expect(getNumberOfUrlSegments(input)).toEqual(output); }); }); diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 5be557af86c5..415062811fb8 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -42,8 +42,10 @@ export { flush, close, getSentryRelease, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, DEFAULT_USER_INCLUDES, + // eslint-disable-next-line deprecation/deprecation extractRequestData, createGetModuleFromFilename, anrIntegration, diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 10e1d94566af..5d8c6c46b911 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -6,6 +6,7 @@ import type { Client, EventProcessor, Integration } from '@sentry/types'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; +import { isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import { browserTracingIntegration } from './browserTracingIntegration'; import { nextjsClientStackFrameNormalizationIntegration } from './clientNormalizationIntegration'; import { INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME } from './routing/appRouterRoutingInstrumentation'; @@ -47,6 +48,11 @@ export function init(options: BrowserOptions): Client | undefined { filterIncompleteNavigationTransactions.id = 'IncompleteTransactionFilter'; addEventProcessor(filterIncompleteNavigationTransactions); + const filterNextRedirectError: EventProcessor = (event, hint) => + isRedirectNavigationError(hint?.originalException) ? null : event; + filterNextRedirectError.id = 'NextRedirectErrorFilter'; + addEventProcessor(filterNextRedirectError); + if (process.env.NODE_ENV === 'development') { addEventProcessor(devErrorSymbolicationEventProcessor); } diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 026622318e3f..445ef32ab1e3 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { arrayify, escapeStringForRegex, loadModule, logger } from '@sentry/core'; +import { escapeStringForRegex, loadModule, logger } from '@sentry/core'; import { getSentryRelease } from '@sentry/node'; import * as chalk from 'chalk'; import { sync as resolveSync } from 'resolve'; @@ -491,7 +491,7 @@ function addFilesToWebpackEntryPoint( let newEntryPoint = currentEntryPoint; if (typeof currentEntryPoint === 'string' || Array.isArray(currentEntryPoint)) { - newEntryPoint = arrayify(currentEntryPoint); + newEntryPoint = Array.isArray(currentEntryPoint) ? currentEntryPoint : [currentEntryPoint]; if (newEntryPoint.some(entry => filesToInsert.includes(entry))) { return; } @@ -507,7 +507,7 @@ function addFilesToWebpackEntryPoint( // descriptor object (webpack 5+) else if (typeof currentEntryPoint === 'object' && 'import' in currentEntryPoint) { const currentImportValue = currentEntryPoint.import; - const newImportValue = arrayify(currentImportValue); + const newImportValue = Array.isArray(currentImportValue) ? currentImportValue : [currentImportValue]; if (newImportValue.some(entry => filesToInsert.includes(entry))) { return; } diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 2df1e48767ce..df6cce5383a3 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -57,6 +57,7 @@ export { cron } from './cron'; export type { NodeOptions } from './types'; +// eslint-disable-next-line deprecation/deprecation export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/core'; export { diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 1fa2218d5879..0777940cc211 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -1,4 +1,3 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; import { Worker } from 'node:worker_threads'; import { defineIntegration, getCurrentScope, getGlobalScope, getIsolationScope, mergeScopeData } from '@sentry/core'; import { GLOBAL_OBJ, getFilenameToDebugIdMap, logger } from '@sentry/core'; @@ -101,13 +100,6 @@ type AnrReturn = (options?: Partial) => Integration & Anr export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn; -function onModuleLoad(callback: () => void): void { - // eslint-disable-next-line deprecation/deprecation - diagnosticsChannel.channel('module.require.end').subscribe(() => callback()); - // eslint-disable-next-line deprecation/deprecation - diagnosticsChannel.channel('module.import.asyncEnd').subscribe(() => callback()); -} - /** * Starts the ANR worker thread * @@ -161,12 +153,6 @@ async function _startWorker( } } - let debugImages: Record = getFilenameToDebugIdMap(initOptions.stackParser); - - onModuleLoad(() => { - debugImages = getFilenameToDebugIdMap(initOptions.stackParser); - }); - const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), { workerData: options, // We don't want any Node args to be passed to the worker @@ -185,7 +171,7 @@ async function _startWorker( // serialized without making it a SerializedSession const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; // message the worker to tell it the main event loop is still running - worker.postMessage({ session, debugImages }); + worker.postMessage({ session, debugImages: getFilenameToDebugIdMap(initOptions.stackParser) }); } catch (_) { // } diff --git a/packages/node/src/sdk/client.ts b/packages/node/src/sdk/client.ts index 8179d40e2819..f8a775c15600 100644 --- a/packages/node/src/sdk/client.ts +++ b/packages/node/src/sdk/client.ts @@ -1,10 +1,12 @@ import * as os from 'node:os'; import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import type { ServerRuntimeClientOptions } from '@sentry/core'; -import { SDK_VERSION, ServerRuntimeClient, applySdkMetadata } from '@sentry/core'; -import { logger } from '@sentry/core'; +import type { Scope, ServerRuntimeClientOptions } from '@sentry/core'; +import { SDK_VERSION, ServerRuntimeClient, applySdkMetadata, logger } from '@sentry/core'; +import { getTraceContextForScope } from '@sentry/opentelemetry'; +import type { DynamicSamplingContext, TraceContext } from '@sentry/types'; import { isMainThread, threadId } from 'worker_threads'; import { DEBUG_BUILD } from '../debug-build'; import type { NodeClientOptions } from '../types'; @@ -26,6 +28,12 @@ export class NodeClient extends ServerRuntimeClient { serverName: options.serverName || global.process.env.SENTRY_NAME || os.hostname(), }; + if (options.openTelemetryInstrumentations) { + registerInstrumentations({ + instrumentations: options.openTelemetryInstrumentations, + }); + } + applySdkMetadata(clientOptions, 'node'); logger.log( @@ -115,4 +123,15 @@ export class NodeClient extends ServerRuntimeClient { process.on('beforeExit', this._clientReportOnExitFlushListener); } } + + /** Custom implementation for OTEL, so we can handle scope-span linking. */ + protected _getTraceInfoFromScope( + scope: Scope | undefined, + ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { + if (!scope) { + return [undefined, undefined]; + } + + return getTraceContextForScope(this, scope); + } } diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index fd37e5ef477f..b731ecd8a332 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -42,10 +42,12 @@ interface RegisterOptions { } function getRegisterOptions(esmHookConfig?: EsmLoaderHookOptions): RegisterOptions { + // TODO(v9): Make onlyIncludeInstrumentedModules: true the default behavior. if (esmHookConfig?.onlyIncludeInstrumentedModules) { const { addHookMessagePort } = createAddHookMessageChannel(); // If the user supplied include, we need to use that as a starting point or use an empty array to ensure no modules // are wrapped if they are not hooked + // eslint-disable-next-line deprecation/deprecation return { data: { addHookMessagePort, include: esmHookConfig.include || [] }, transferList: [addHookMessagePort] }; } @@ -75,7 +77,7 @@ export function maybeInitializeEsmLoader(esmHookConfig?: EsmLoaderHookOptions): consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( - '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or use version 7.x of the Sentry Node.js SDK.', + '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or upgrade your Node.js version.', ); }); } diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 7235e2057c34..512e6c164727 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -1,12 +1,30 @@ import type { Span as WriteableSpan } from '@opentelemetry/api'; +import type { Instrumentation } from '@opentelemetry/instrumentation'; import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/types'; import type { NodeTransportOptions } from './transports'; +/** + * Note: In the next major version of the Sentry SDK this interface will be removed and the SDK will by default only wrap + * ESM modules that are required to be wrapped by OpenTelemetry Instrumentation. + */ export interface EsmLoaderHookOptions { + /** + * Provide a list of modules to wrap with `import-in-the-middle`. + * + * @deprecated It is recommended to use `onlyIncludeInstrumentedModules: true` instead of manually defining modules to include and exclude. + */ include?: Array; - exclude?: Array /** + + /** + * Provide a list of modules to prevent them from being wrapped with `import-in-the-middle`. + * + * @deprecated It is recommended to use `onlyIncludeInstrumentedModules: true` instead of manually defining modules to include and exclude. + */ + exclude?: Array; + + /** * When set to `true`, `import-in-the-middle` will only wrap ESM modules that are specifically instrumented by * OpenTelemetry plugins. This is useful to avoid issues where `import-in-the-middle` is not compatible with some of * your dependencies. @@ -16,7 +34,11 @@ export interface EsmLoaderHookOptions { * `Sentry.init()`. * * Defaults to `false`. - */; + * + * Note: In the next major version of the Sentry SDK this option will be removed and the SDK will by default only wrap + * ESM modules that are required to be wrapped by OpenTelemetry Instrumentation. + */ + // TODO(v9): Make `onlyIncludeInstrumentedModules: true` the default behavior. onlyIncludeInstrumentedModules?: boolean; } @@ -87,9 +109,18 @@ export interface BaseNodeOptions { * * The `SentryPropagator` * * The `SentryContextManager` * * The `SentrySampler` + * + * If you are registering your own OpenTelemetry Loader Hooks (or `import-in-the-middle` hooks), it is also recommended to set the `registerEsmLoaderHooks` option to false. */ skipOpenTelemetrySetup?: boolean; + /** + * Provide an array of OpenTelemetry Instrumentations that should be registered. + * + * Use this option if you want to register OpenTelemetry instrumentation that the Sentry SDK does not yet have support for. + */ + openTelemetryInstrumentations?: Instrumentation[]; + /** * The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span. * The SDK will automatically clean up spans that have no finished parent after this duration. @@ -117,7 +148,11 @@ export interface BaseNodeOptions { * ``` * * Defaults to `true`. + * + * Note: In the next major version of the SDK, the possibility to provide fine-grained control will be removed from this option. + * This means that it will only be possible to pass `true` or `false`. The default value will continue to be `true`. */ + // TODO(v9): Only accept true | false | undefined. registerEsmLoaderHooks?: boolean | EsmLoaderHookOptions; /** @@ -156,7 +191,7 @@ export interface CurrentScopes { * The base `Span` type is basically a `WriteableSpan`. * There are places where we basically want to allow passing _any_ span, * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. - * You'll have to make sur to check revelant fields before accessing them. + * You'll have to make sur to check relevant fields before accessing them. * * Note that technically, the `Span` exported from `@opentelemetry/sdk-trace-base` matches this, * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. diff --git a/packages/node/src/utils/redisCache.ts b/packages/node/src/utils/redisCache.ts index cb411f304cf7..2a963710e0d5 100644 --- a/packages/node/src/utils/redisCache.ts +++ b/packages/node/src/utils/redisCache.ts @@ -1,5 +1,4 @@ import type { CommandArgs as IORedisCommandArgs } from '@opentelemetry/instrumentation-ioredis'; -import { flatten } from '@sentry/core'; const SINGLE_ARG_COMMANDS = ['get', 'set', 'setex']; @@ -95,3 +94,23 @@ export function calculateCacheItemSize(response: unknown): number | undefined { }, 0) : getSize(response); } + +// TODO(v9): This is inlined from core so we can deprecate `flatten`. +// It's usage can be replaced with `Array.flat` in v9. +type NestedArray = Array | T>; +function flatten(input: NestedArray): T[] { + const result: T[] = []; + + const flattenHelper = (input: NestedArray): void => { + input.forEach((el: T | NestedArray) => { + if (Array.isArray(el)) { + flattenHelper(el as NestedArray); + } else { + result.push(el as T); + } + }); + }; + + flattenHelper(input); + return result; +} diff --git a/packages/node/test/integration/transactions.test.ts b/packages/node/test/integration/transactions.test.ts index e13d239821d3..a9d524ea0285 100644 --- a/packages/node/test/integration/transactions.test.ts +++ b/packages/node/test/integration/transactions.test.ts @@ -577,17 +577,9 @@ describe('Integration | Transactions', () => { throw new Error('No exporter found, aborting test...'); } - let innerSpan1Id: string | undefined; - let innerSpan2Id: string | undefined; - void Sentry.startSpan({ name: 'test name' }, async () => { - const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); - innerSpan1Id = subSpan.spanContext().spanId; - subSpan.end(); - - Sentry.startSpan({ name: 'inner span 2' }, innerSpan => { - innerSpan2Id = innerSpan.spanContext().spanId; - }); + Sentry.startInactiveSpan({ name: 'inner span 1' }).end(); + Sentry.startInactiveSpan({ name: 'inner span 2' }).end(); // Pretend this is pending for 10 minutes await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); @@ -596,7 +588,13 @@ describe('Integration | Transactions', () => { jest.advanceTimersByTime(1); // Child-spans have been added to the exporter, but they are pending since they are waiting for their parent - expect(exporter['_finishedSpans'].length).toBe(2); + const finishedSpans1 = []; + exporter['_finishedSpanBuckets'].forEach((bucket: any) => { + if (bucket) { + finishedSpans1.push(...bucket.spans); + } + }); + expect(finishedSpans1.length).toBe(2); expect(beforeSendTransaction).toHaveBeenCalledTimes(0); // Now wait for 5 mins @@ -608,18 +606,21 @@ describe('Integration | Transactions', () => { jest.advanceTimersByTime(1); // Old spans have been cleared away - expect(exporter['_finishedSpans'].length).toBe(0); + const finishedSpans2 = []; + exporter['_finishedSpanBuckets'].forEach((bucket: any) => { + if (bucket) { + finishedSpans2.push(...bucket.spans); + } + }); + expect(finishedSpans2.length).toBe(0); // Called once for the 'other span' expect(beforeSendTransaction).toHaveBeenCalledTimes(1); expect(logs).toEqual( expect.arrayContaining([ - 'SpanExporter has 1 unsent spans remaining', - 'SpanExporter has 2 unsent spans remaining', - 'SpanExporter exported 1 spans, 2 unsent spans remaining', - `SpanExporter dropping span inner span 1 (${innerSpan1Id}) because it is pending for more than 5 minutes.`, - `SpanExporter dropping span inner span 2 (${innerSpan2Id}) because it is pending for more than 5 minutes.`, + 'SpanExporter dropped 2 spans because they were pending for more than 300 seconds.', + 'SpanExporter exported 1 spans, 0 unsent spans remaining', ]), ); }); diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index 1aa4cf14fdff..8f85c375592f 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -1,5 +1,6 @@ import * as os from 'os'; import { ProxyTracer } from '@opentelemetry/api'; +import * as opentelemetryInstrumentationPackage from '@opentelemetry/instrumentation'; import { SDK_VERSION, SessionFlusher, @@ -495,6 +496,21 @@ describe('NodeClient', () => { expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0); }); }); + + it('registers instrumentations provided with `openTelemetryInstrumentations`', () => { + const registerInstrumentationsSpy = jest + .spyOn(opentelemetryInstrumentationPackage, 'registerInstrumentations') + .mockImplementationOnce(() => () => undefined); + const instrumentationsArray = ['foobar'] as unknown as opentelemetryInstrumentationPackage.Instrumentation[]; + + new NodeClient(getDefaultNodeClientOptions({ openTelemetryInstrumentations: instrumentationsArray })); + + expect(registerInstrumentationsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + instrumentations: instrumentationsArray, + }), + ); + }); }); describe('flush/close', () => { diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index df5f2b285ddd..225517786c03 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -39,7 +39,7 @@ "access": "public" }, "peerDependencies": { - "nuxt": "3.x" + "nuxt": ">=3.7.0 || 4.x" }, "dependencies": { "@nuxt/kit": "^3.13.2", diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 5a684998da5a..bd6cb96122de 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -13,7 +13,7 @@ export default defineNuxtModule({ name: '@sentry/nuxt/module', configKey: 'sentry', compatibility: { - nuxt: '^3.0.0', + nuxt: '>=3.7.0', }, }, defaults: {}, diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 81ab93e6ccb2..ccef93ea3350 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -26,30 +26,52 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { const client = initNode(sentryOptions); - getGlobalScope().addEventProcessor( - Object.assign( - (event => { - if (event.type === 'transaction') { - // Filter out transactions for Nuxt build assets - // This regex matches the default path to the nuxt-generated build assets (`_nuxt`). - // todo: the buildAssetDir could be changed in the nuxt config - change this to a more generic solution - if (event.transaction?.match(/^GET \/_nuxt\//)) { - options.debug && - DEBUG_BUILD && - logger.log('NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); - return null; - } + getGlobalScope().addEventProcessor(lowQualityTransactionsFilter(options)); + getGlobalScope().addEventProcessor(clientSourceMapErrorFilter(options)); - return event; - } else { - return event; - } - }) satisfies EventProcessor, - { id: 'NuxtLowQualityTransactionsFilter' }, - ), + return client; +} + +/** + * Filter out transactions for Nuxt build assets + * This regex matches the default path to the nuxt-generated build assets (`_nuxt`). + * + * Only exported for testing + */ +export function lowQualityTransactionsFilter(options: SentryNuxtServerOptions): EventProcessor { + return Object.assign( + (event => { + if (event.type === 'transaction' && event.transaction?.match(/^GET \/_nuxt\//)) { + // todo: the buildAssetDir could be changed in the nuxt config - change this to a more generic solution + options.debug && + DEBUG_BUILD && + logger.log('NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); + return null; + } else { + return event; + } + }) satisfies EventProcessor, + { id: 'NuxtLowQualityTransactionsFilter' }, ); +} - return client; +/** + * The browser devtools try to get the source maps, but as client source maps may not be available there is going to be an error (no problem for the application though). + * + * Only exported for testing + */ +export function clientSourceMapErrorFilter(options: SentryNuxtServerOptions): EventProcessor { + return Object.assign( + (event => { + const errorMsg = event.exception?.values?.[0]?.value; + if (errorMsg?.match(/^ENOENT: no such file or directory, open '.*\/_nuxt\/.*\.js\.map'/)) { + options.debug && DEBUG_BUILD && logger.log('NuxtClientSourceMapErrorFilter filtered error: ', errorMsg); + return null; + } + return event; + }) satisfies EventProcessor, + { id: 'NuxtClientSourceMapErrorFilter' }, + ); } function getNuxtDefaultIntegrations(options: NodeOptions): Integration[] { @@ -78,9 +100,12 @@ export function mergeRegisterEsmLoaderHooks( ): SentryNuxtServerOptions['registerEsmLoaderHooks'] { if (typeof options.registerEsmLoaderHooks === 'object' && options.registerEsmLoaderHooks !== null) { return { + // eslint-disable-next-line deprecation/deprecation exclude: Array.isArray(options.registerEsmLoaderHooks.exclude) - ? [...options.registerEsmLoaderHooks.exclude, /vue/] - : options.registerEsmLoaderHooks.exclude ?? [/vue/], + ? // eslint-disable-next-line deprecation/deprecation + [...options.registerEsmLoaderHooks.exclude, /vue/] + : // eslint-disable-next-line deprecation/deprecation + options.registerEsmLoaderHooks.exclude ?? [/vue/], }; } return options.registerEsmLoaderHooks ?? { exclude: [/vue/] }; diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index beea7e18d8f2..2f90094e6138 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -81,7 +81,7 @@ export function getPluginOptions( consoleSandbox(() => { // eslint-disable-next-line no-console console.log( - '[Sentry] Setting `sentry.sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [".*/**/*.map"]` to delete generated source maps after they were uploaded to Sentry.', + '[Sentry] Setting `sentry.sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [".*/**/public/**/*.map"]` to delete generated source maps after they were uploaded to Sentry.', ); }); } @@ -108,7 +108,7 @@ export function getPluginOptions( filesToDeleteAfterUpload: sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload ? sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload : deleteFilesAfterUpload - ? ['.*/**/*.map'] + ? ['.*/**/public/**/*.map'] : undefined, rewriteSources: (source: string) => normalizePath(source), ...moduleOptions?.unstable_sentryBundlerPluginOptions?.sourcemaps, @@ -279,7 +279,7 @@ function warnExplicitlyDisabledSourceMap(settingKey: string): void { consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( - `[Sentry] Parts of source map generation are currently disabled in your Nuxt configuration (\`${settingKey}: false\`). This setting is either a default setting or was explicitly set in your configuration. Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified. To show unminified code, enable source maps in \`${settingKey}\`.`, + `[Sentry] Parts of source map generation are currently disabled in your Nuxt configuration (\`${settingKey}: false\`). This setting is either a default setting or was explicitly set in your configuration. Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified. To show unminified code, enable source maps in \`${settingKey}\` (e.g. by setting them to \`hidden\`).`, ); }); } diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index d18fd1dcc484..fff676a6ede1 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { consoleSandbox, flatten } from '@sentry/core'; +import { consoleSandbox } from '@sentry/core'; /** * Find the default SDK init file for the given type (client or server). @@ -92,18 +92,21 @@ export function constructWrappedFunctionExportQuery( entrypointWrappedFunctions: string[], debug?: boolean, ): string { + const functionsToExport: { wrap: string[]; reexport: string[] } = { + wrap: [], + reexport: [], + }; + // `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }` // The key `.` refers to exports within the current file, while other keys show from where exports were imported first. - const functionsToExport = flatten(Object.values(exportedBindings || {})).reduce( - (functions, currFunctionName) => { - if (entrypointWrappedFunctions.includes(currFunctionName)) { - functions.wrap.push(currFunctionName); + Object.values(exportedBindings || {}).forEach(functions => + functions.forEach(fn => { + if (entrypointWrappedFunctions.includes(fn)) { + functionsToExport.wrap.push(fn); } else { - functions.reexport.push(currFunctionName); + functionsToExport.reexport.push(fn); } - return functions; - }, - { wrap: [], reexport: [] } as { wrap: string[]; reexport: string[] }, + }), ); if (debug && functionsToExport.wrap.length === 0) { diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index 7ff68478e36d..56888afc9a79 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -1,10 +1,13 @@ import * as SentryNode from '@sentry/node'; import type { NodeClient } from '@sentry/node'; +import { Scope } from '@sentry/node'; +import { getGlobalScope } from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; +import type { EventProcessor } from '@sentry/types'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { SentryNuxtServerOptions } from '../../src/common/types'; import { init } from '../../src/server'; -import { mergeRegisterEsmLoaderHooks } from '../../src/server/sdk'; +import { clientSourceMapErrorFilter, mergeRegisterEsmLoaderHooks } from '../../src/server/sdk'; const nodeInit = vi.spyOn(SentryNode, 'init'); @@ -83,6 +86,88 @@ describe('Nuxt Server SDK', () => { expect.any(Object), ); }); + + it('registers an event processor', async () => { + let passedEventProcessors: EventProcessor[] = []; + const addEventProcessor = vi + .spyOn(getGlobalScope(), 'addEventProcessor') + .mockImplementation((eventProcessor: EventProcessor) => { + passedEventProcessors = [...passedEventProcessors, eventProcessor]; + return new Scope(); + }); + + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); + + expect(addEventProcessor).toHaveBeenCalledTimes(2); + expect(passedEventProcessors[0]?.id).toEqual('NuxtLowQualityTransactionsFilter'); + expect(passedEventProcessors[1]?.id).toEqual('NuxtClientSourceMapErrorFilter'); + }); + }); + + describe('clientSourceMapErrorFilter', () => { + const options = { debug: false }; + const filter = clientSourceMapErrorFilter(options); + + describe('filters out errors', () => { + it.each([ + [ + 'source map errors with leading /', + { + exception: { values: [{ value: "ENOENT: no such file or directory, open '/path/to/_nuxt/file.js.map'" }] }, + }, + ], + [ + 'source map errors without leading /', + { exception: { values: [{ value: "ENOENT: no such file or directory, open 'path/to/_nuxt/file.js.map'" }] } }, + ], + [ + 'source map errors with long path', + { + exception: { + values: [ + { + value: + "ENOENT: no such file or directory, open 'path/to/public/_nuxt/public/long/long/path/file.js.map'", + }, + ], + }, + }, + ], + ])('filters out %s', (_, event) => { + // @ts-expect-error Event type is not correct in tests + expect(filter(event)).toBeNull(); + }); + }); + + describe('does not filter out errors', () => { + it.each([ + ['other errors', { exception: { values: [{ value: 'Some other error' }] } }], + ['events with no exceptions', {}], + [ + 'events without _nuxt in path', + { + exception: { values: [{ value: "ENOENT: no such file or directory, open '/path/to/other/file.js.map'" }] }, + }, + ], + [ + 'source map errors with different casing', + { + exception: { values: [{ value: "ENOENT: No Such file or directory, open '/path/to/_nuxt/file.js.map'" }] }, + }, + ], + [ + 'non-source-map file', + { exception: { values: [{ value: "ENOENT: no such file or directory, open '/path/to/_nuxt/file.js'" }] } }, + ], + ['events with no exception values', { exception: { values: [] } }], + ['events with null exception value', { exception: { values: [null] } }], + ])('does not filter out %s', (_, event) => { + // @ts-expect-error Event type is not correct in tests + expect(filter(event)).toEqual(event); + }); + }); }); describe('mergeRegisterEsmLoaderHooks', () => { diff --git a/packages/opentelemetry/src/index.ts b/packages/opentelemetry/src/index.ts index 98460b575c8d..55f657061989 100644 --- a/packages/opentelemetry/src/index.ts +++ b/packages/opentelemetry/src/index.ts @@ -27,7 +27,14 @@ export { enhanceDscWithOpenTelemetryRootSpanName } from './utils/enhanceDscWithO export { generateSpanContextForPropagationContext } from './utils/generateSpanContextForPropagationContext'; export { getActiveSpan } from './utils/getActiveSpan'; -export { startSpan, startSpanManual, startInactiveSpan, withActiveSpan, continueTrace } from './trace'; +export { + startSpan, + startSpanManual, + startInactiveSpan, + withActiveSpan, + continueTrace, + getTraceContextForScope, +} from './trace'; export { suppressTracing } from './utils/suppressTracing'; diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 054a348fd7b5..6c4009888416 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -5,15 +5,10 @@ import { propagation, trace } from '@opentelemetry/api'; import { W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; import { ATTR_URL_FULL, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; import type { continueTrace } from '@sentry/core'; +import { getDynamicSamplingContextFromScope } from '@sentry/core'; import { getRootSpan } from '@sentry/core'; import { spanToJSON } from '@sentry/core'; -import { - getClient, - getCurrentScope, - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - getIsolationScope, -} from '@sentry/core'; +import { getClient, getCurrentScope, getDynamicSamplingContextFromSpan, getIsolationScope } from '@sentry/core'; import { LRUMap, SENTRY_BAGGAGE_KEY_PREFIX, @@ -191,7 +186,10 @@ export class SentryPropagator extends W3CBaggagePropagator { } } -function getInjectionData(context: Context): { +/** + * Get propagation injection data for the given context. + */ +export function getInjectionData(context: Context): { dynamicSamplingContext: Partial | undefined; traceId: string | undefined; spanId: string | undefined; @@ -204,8 +202,7 @@ function getInjectionData(context: Context): { if (span && !spanIsRemote) { const spanContext = span.spanContext(); - const propagationContext = getPropagationContextFromSpan(span); - const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, spanContext.traceId); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); return { dynamicSamplingContext, traceId: spanContext.traceId, @@ -216,9 +213,10 @@ function getInjectionData(context: Context): { // Else we try to use the propagation context from the scope const scope = getScopesFromContext(context)?.scope || getCurrentScope(); + const client = getClient(); const propagationContext = scope.getPropagationContext(); - const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, propagationContext.traceId); + const dynamicSamplingContext = client ? getDynamicSamplingContextFromScope(client, scope) : undefined; return { dynamicSamplingContext, traceId: propagationContext.traceId, @@ -227,26 +225,6 @@ function getInjectionData(context: Context): { }; } -/** Get the DSC from a context, or fall back to use the one from the client. */ -function getDynamicSamplingContext( - propagationContext: PropagationContext, - traceId: string | undefined, -): Partial | undefined { - // If we have a DSC on the propagation context, we just use it - if (propagationContext?.dsc) { - return propagationContext.dsc; - } - - // Else, we try to generate a new one - const client = getClient(); - - if (client) { - return getDynamicSamplingContextFromClient(traceId || propagationContext.traceId, client); - } - - return undefined; -} - function getContextWithRemoteActiveSpan( ctx: Context, { sentryTrace, baggage }: Parameters[0], diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 11d822f66ec1..91a8390100df 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -35,60 +35,121 @@ type SpanNodeCompleted = SpanNode & { span: ReadableSpan }; const MAX_SPAN_COUNT = 1000; const DEFAULT_TIMEOUT = 300; // 5 min +interface FinishedSpanBucket { + timestampInS: number; + spans: Set; +} + /** * A Sentry-specific exporter that converts OpenTelemetry Spans to Sentry Spans & Transactions. */ export class SentrySpanExporter { private _flushTimeout: ReturnType | undefined; - private _finishedSpans: ReadableSpan[]; - private _timeout: number; - public constructor(options?: { timeout?: number }) { - this._finishedSpans = []; - this._timeout = options?.timeout || DEFAULT_TIMEOUT; + /* + * A quick explanation on the buckets: We do bucketing of finished spans for efficiency. This span exporter is + * accumulating spans until a root span is encountered and then it flushes all the spans that are descendants of that + * root span. Because it is totally in the realm of possibilities that root spans are never finished, and we don't + * want to accumulate spans indefinitely in memory, we need to periodically evacuate spans. Naively we could simply + * store the spans in an array and each time a new span comes in we could iterate through the entire array and + * evacuate all spans that have an end-timestamp that is older than our limit. This could get quite expensive because + * we would have to iterate a potentially large number of spans every time we evacuate. We want to avoid these large + * bursts of computation. + * + * Instead we go for a bucketing approach and put spans into buckets, based on what second + * (modulo the time limit) the span was put into the exporter. With buckets, when we decide to evacuate, we can + * iterate through the bucket entries instead, which have an upper bound of items, making the evacuation much more + * efficient. Cleaning up also becomes much more efficient since it simply involves de-referencing a bucket within the + * bucket array, and letting garbage collection take care of the rest. + */ + private _finishedSpanBuckets: (FinishedSpanBucket | undefined)[]; + private _finishedSpanBucketSize: number; + private _spansToBucketEntry: WeakMap; + private _lastCleanupTimestampInS: number; + + public constructor(options?: { + /** Lower bound of time in seconds until spans that are buffered but have not been sent as part of a transaction get cleared from memory. */ + timeout?: number; + }) { + this._finishedSpanBucketSize = options?.timeout || DEFAULT_TIMEOUT; + this._finishedSpanBuckets = new Array(this._finishedSpanBucketSize).fill(undefined); + this._lastCleanupTimestampInS = Math.floor(Date.now() / 1000); + this._spansToBucketEntry = new WeakMap(); } /** Export a single span. */ public export(span: ReadableSpan): void { - this._finishedSpans.push(span); - - // If the span has a local parent ID, we don't need to export anything just yet - if (getLocalParentId(span)) { - const openSpanCount = this._finishedSpans.length; - DEBUG_BUILD && logger.log(`SpanExporter has ${openSpanCount} unsent spans remaining`); - this._cleanupOldSpans(); - return; + const currentTimestampInS = Math.floor(Date.now() / 1000); + + if (this._lastCleanupTimestampInS !== currentTimestampInS) { + let droppedSpanCount = 0; + this._finishedSpanBuckets.forEach((bucket, i) => { + if (bucket && bucket.timestampInS <= currentTimestampInS - this._finishedSpanBucketSize) { + droppedSpanCount += bucket.spans.size; + this._finishedSpanBuckets[i] = undefined; + } + }); + if (droppedSpanCount > 0) { + DEBUG_BUILD && + logger.log( + `SpanExporter dropped ${droppedSpanCount} spans because they were pending for more than ${this._finishedSpanBucketSize} seconds.`, + ); + } + this._lastCleanupTimestampInS = currentTimestampInS; } - this._clearTimeout(); - - // If we got a parent span, we try to send the span tree - // Wait a tick for this, to ensure we avoid race conditions - this._flushTimeout = setTimeout(() => { - this.flush(); - }, 1); + const currentBucketIndex = currentTimestampInS % this._finishedSpanBucketSize; + const currentBucket = this._finishedSpanBuckets[currentBucketIndex] || { + timestampInS: currentTimestampInS, + spans: new Set(), + }; + this._finishedSpanBuckets[currentBucketIndex] = currentBucket; + currentBucket.spans.add(span); + this._spansToBucketEntry.set(span, currentBucket); + + // If the span doesn't have a local parent ID (it's a root span), we're gonna flush all the ended spans + if (!getLocalParentId(span)) { + this._clearTimeout(); + + // If we got a parent span, we try to send the span tree + // Wait a tick for this, to ensure we avoid race conditions + this._flushTimeout = setTimeout(() => { + this.flush(); + }, 1); + } } /** Try to flush any pending spans immediately. */ public flush(): void { this._clearTimeout(); - const openSpanCount = this._finishedSpans.length; + const finishedSpans: ReadableSpan[] = []; + this._finishedSpanBuckets.forEach(bucket => { + if (bucket) { + finishedSpans.push(...bucket.spans); + } + }); + + const sentSpans = maybeSend(finishedSpans); - const remainingSpans = maybeSend(this._finishedSpans); + const sentSpanCount = sentSpans.size; - const remainingOpenSpanCount = remainingSpans.length; - const sentSpanCount = openSpanCount - remainingOpenSpanCount; + const remainingOpenSpanCount = finishedSpans.length - sentSpanCount; DEBUG_BUILD && logger.log(`SpanExporter exported ${sentSpanCount} spans, ${remainingOpenSpanCount} unsent spans remaining`); - this._cleanupOldSpans(remainingSpans); + sentSpans.forEach(span => { + const bucketEntry = this._spansToBucketEntry.get(span); + if (bucketEntry) { + bucketEntry.spans.delete(span); + } + }); } /** Clear the exporter. */ public clear(): void { - this._finishedSpans = []; + this._finishedSpanBuckets = this._finishedSpanBuckets.fill(undefined); this._clearTimeout(); } @@ -99,52 +160,33 @@ export class SentrySpanExporter { this._flushTimeout = undefined; } } - - /** - * Remove any span that is older than 5min. - * We do this to avoid leaking memory. - */ - private _cleanupOldSpans(spans = this._finishedSpans): void { - const currentTimeSeconds = Date.now() / 1000; - this._finishedSpans = spans.filter(span => { - const shouldDrop = shouldCleanupSpan(span, currentTimeSeconds, this._timeout); - DEBUG_BUILD && - shouldDrop && - logger.log( - `SpanExporter dropping span ${span.name} (${ - span.spanContext().spanId - }) because it is pending for more than 5 minutes.`, - ); - return !shouldDrop; - }); - } } /** * Send the given spans, but only if they are part of a finished transaction. * - * Returns the unsent spans. + * Returns the sent spans. * Spans remain unsent when their parent span is not yet finished. * This will happen regularly, as child spans are generally finished before their parents. * But it _could_ also happen because, for whatever reason, a parent span was lost. * In this case, we'll eventually need to clean this up. */ -function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { +function maybeSend(spans: ReadableSpan[]): Set { const grouped = groupSpansWithParents(spans); - const remaining = new Set(grouped); + const sentSpans = new Set(); const rootNodes = getCompletedRootNodes(grouped); rootNodes.forEach(root => { - remaining.delete(root); const span = root.span; + sentSpans.add(span); const transactionEvent = createTransactionForOtelSpan(span); // We'll recursively add all the child spans to this array const spans = transactionEvent.spans || []; root.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, spans, remaining); + createAndFinishSpanForOtelSpan(child, spans, sentSpans); }); // spans.sort() mutates the array, but we do not use this anymore after this point @@ -162,9 +204,7 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { captureEvent(transactionEvent); }); - return Array.from(remaining) - .map(node => node.span) - .filter((span): span is ReadableSpan => !!span); + return sentSpans; } function nodeIsCompletedRootNode(node: SpanNode): node is SpanNodeCompleted { @@ -175,11 +215,6 @@ function getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { return nodes.filter(nodeIsCompletedRootNode); } -function shouldCleanupSpan(span: ReadableSpan, currentTimeSeconds: number, maxStartTimeOffsetSeconds: number): boolean { - const cutoff = currentTimeSeconds - maxStartTimeOffsetSeconds; - return spanTimeInputToSeconds(span.startTime) < cutoff; -} - function parseSpan(span: ReadableSpan): { op?: string; origin?: SpanOrigin; source?: TransactionSource } { const attributes = span.attributes; @@ -260,16 +295,19 @@ function createTransactionForOtelSpan(span: ReadableSpan): TransactionEvent { return transactionEvent; } -function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], remaining: Set): void { - remaining.delete(node); +function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentSpans: Set): void { const span = node.span; + if (span) { + sentSpans.add(span); + } + const shouldDrop = !span; // If this span should be dropped, we still want to create spans for the children of this if (shouldDrop) { node.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, spans, remaining); + createAndFinishSpanForOtelSpan(child, spans, sentSpans); }); return; } @@ -308,7 +346,7 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], remai spans.push(spanJSON); node.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, spans, remaining); + createAndFinishSpanForOtelSpan(child, spans, sentSpans); }); } diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts index 6ba41eec51e2..3fa994a74de8 100644 --- a/packages/opentelemetry/src/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -7,12 +7,15 @@ import { continueTrace as baseContinueTrace, getClient, getCurrentScope, + getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan, getRootSpan, + getTraceContextFromScope, handleCallbackErrors, spanToJSON, + spanToTraceContext, } from '@sentry/core'; -import type { Client, Scope, Span as SentrySpan } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Scope, Span as SentrySpan, TraceContext } from '@sentry/types'; import { continueTraceAsRemoteSpan } from './propagator'; import type { OpenTelemetryClient, OpenTelemetrySpanContext } from './types'; @@ -176,8 +179,9 @@ function ensureTimestampInMilliseconds(timestamp: number): number { function getContext(scope: Scope | undefined, forceTransaction: boolean | undefined): Context { const ctx = getContextForScope(scope); + // Note: If the context is the ROOT_CONTEXT, no scope is attached + // Thus we will not use the propagation context in this case, which is desired const actualScope = getScopesFromContext(ctx)?.scope; - const parentSpan = trace.getSpan(ctx); // In the case that we have no parent span, we need to "simulate" one to ensure the propagation context is correct @@ -278,6 +282,25 @@ export function continueTrace(options: Parameters[0 }); } +/** + * Get the trace context for a given scope. + * We have a custom implemention here because we need an OTEL-specific way to get the span from a scope. + */ +export function getTraceContextForScope( + client: Client, + scope: Scope, +): [dynamicSamplingContext: Partial, traceContext: TraceContext] { + const ctx = getContextFromScope(scope); + const span = ctx && trace.getSpan(ctx); + + const traceContext = span ? spanToTraceContext(span) : getTraceContextFromScope(scope); + + const dynamicSamplingContext = span + ? getDynamicSamplingContextFromSpan(span) + : getDynamicSamplingContextFromScope(client, scope); + return [dynamicSamplingContext, traceContext]; +} + function getActiveSpanWrapper(parentSpan: Span | SentrySpan | undefined | null): (callback: () => T) => T { return parentSpan !== undefined ? (callback: () => T) => { diff --git a/packages/opentelemetry/src/utils/getTraceData.ts b/packages/opentelemetry/src/utils/getTraceData.ts index 8d91c74bd294..c79fc2a6e957 100644 --- a/packages/opentelemetry/src/utils/getTraceData.ts +++ b/packages/opentelemetry/src/utils/getTraceData.ts @@ -1,22 +1,30 @@ import * as api from '@opentelemetry/api'; -import { dropUndefinedKeys } from '@sentry/core'; -import type { SerializedTraceData } from '@sentry/types'; +import { + dynamicSamplingContextToSentryBaggageHeader, + generateSentryTraceHeader, + getCapturedScopesOnSpan, +} from '@sentry/core'; +import type { SerializedTraceData, Span } from '@sentry/types'; +import { getInjectionData } from '../propagator'; +import { getContextFromScope } from './contextData'; /** * Otel-specific implementation of `getTraceData`. * @see `@sentry/core` version of `getTraceData` for more information */ -export function getTraceData(): SerializedTraceData { - const headersObject: Record = {}; +export function getTraceData({ span }: { span?: Span } = {}): SerializedTraceData { + let ctx = api.context.active(); - api.propagation.inject(api.context.active(), headersObject); - - if (!headersObject['sentry-trace']) { - return {}; + if (span) { + const { scope } = getCapturedScopesOnSpan(span); + // fall back to current context if for whatever reason we can't find the one of the span + ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span); } - return dropUndefinedKeys({ - 'sentry-trace': headersObject['sentry-trace'], - baggage: headersObject.baggage, - }); + const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx); + + return { + 'sentry-trace': generateSentryTraceHeader(traceId, spanId, sampled), + baggage: dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext), + }; } diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index b66147a413d7..8dacab4412c0 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -460,24 +460,22 @@ describe('Integration | Transactions', () => { throw new Error('No exporter found, aborting test...'); } - let innerSpan1Id: string | undefined; - let innerSpan2Id: string | undefined; - void startSpan({ name: 'test name' }, async () => { - const subSpan = startInactiveSpan({ name: 'inner span 1' }); - innerSpan1Id = subSpan.spanContext().spanId; - subSpan.end(); - - startSpan({ name: 'inner span 2' }, innerSpan => { - innerSpan2Id = innerSpan.spanContext().spanId; - }); + startInactiveSpan({ name: 'inner span 1' }).end(); + startInactiveSpan({ name: 'inner span 2' }).end(); // Pretend this is pending for 10 minutes await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); }); // Child-spans have been added to the exporter, but they are pending since they are waiting for their parent - expect(exporter['_finishedSpans'].length).toBe(2); + const finishedSpans1 = []; + exporter['_finishedSpanBuckets'].forEach(bucket => { + if (bucket) { + finishedSpans1.push(...bucket.spans); + } + }); + expect(finishedSpans1.length).toBe(2); expect(beforeSendTransaction).toHaveBeenCalledTimes(0); // Now wait for 5 mins @@ -489,18 +487,21 @@ describe('Integration | Transactions', () => { jest.advanceTimersByTime(1); // Old spans have been cleared away - expect(exporter['_finishedSpans'].length).toBe(0); + const finishedSpans2 = []; + exporter['_finishedSpanBuckets'].forEach(bucket => { + if (bucket) { + finishedSpans2.push(...bucket.spans); + } + }); + expect(finishedSpans2.length).toBe(0); // Called once for the 'other span' expect(beforeSendTransaction).toHaveBeenCalledTimes(1); expect(logs).toEqual( expect.arrayContaining([ - 'SpanExporter has 1 unsent spans remaining', - 'SpanExporter has 2 unsent spans remaining', - 'SpanExporter exported 1 spans, 2 unsent spans remaining', - `SpanExporter dropping span inner span 1 (${innerSpan1Id}) because it is pending for more than 5 minutes.`, - `SpanExporter dropping span inner span 2 (${innerSpan2Id}) because it is pending for more than 5 minutes.`, + 'SpanExporter dropped 2 spans because they were pending for more than 300 seconds.', + 'SpanExporter exported 1 spans, 0 unsent spans remaining', ]), ); }); @@ -553,7 +554,13 @@ describe('Integration | Transactions', () => { expect(transactions[0]?.spans).toHaveLength(2); // No spans are pending - expect(exporter['_finishedSpans'].length).toBe(0); + const finishedSpans = []; + exporter['_finishedSpanBuckets'].forEach(bucket => { + if (bucket) { + finishedSpans.push(...bucket.spans); + } + }); + expect(finishedSpans.length).toBe(0); }); it('discards child spans that are finished after their parent span', async () => { @@ -607,8 +614,14 @@ describe('Integration | Transactions', () => { expect(transactions[0]?.spans).toHaveLength(1); // subSpan2 is pending (and will eventually be cleaned up) - expect(exporter['_finishedSpans'].length).toBe(1); - expect(exporter['_finishedSpans'][0]?.name).toBe('inner span 2'); + const finishedSpans: any = []; + exporter['_finishedSpanBuckets'].forEach(bucket => { + if (bucket) { + finishedSpans.push(...bucket.spans); + } + }); + expect(finishedSpans.length).toBe(1); + expect(finishedSpans[0]?.name).toBe('inner span 2'); }); it('uses & inherits DSC on span trace state', async () => { diff --git a/packages/opentelemetry/test/utils/getTraceData.test.ts b/packages/opentelemetry/test/utils/getTraceData.test.ts new file mode 100644 index 000000000000..e0f2270d8e22 --- /dev/null +++ b/packages/opentelemetry/test/utils/getTraceData.test.ts @@ -0,0 +1,93 @@ +import { context, trace } from '@opentelemetry/api'; +import { getCurrentScope, setAsyncContextStrategy } from '@sentry/core'; +import { getTraceData } from '../../src/utils/getTraceData'; +import { makeTraceState } from '../../src/utils/makeTraceState'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; + +describe('getTraceData', () => { + beforeEach(() => { + setAsyncContextStrategy(undefined); + mockSdkInit(); + }); + + afterEach(() => { + cleanupOtel(); + jest.clearAllMocks(); + }); + + it('returns the tracing data from the span, if a span is available', () => { + const ctx = trace.setSpanContext(context.active(), { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: 1, + }); + + context.with(ctx, () => { + const data = getTraceData(); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: + 'sentry-environment=production,sentry-public_key=username,sentry-trace_id=12345678901234567890123456789012,sentry-sampled=true', + }); + }); + }); + + it('allows to pass a span directly', () => { + const ctx = trace.setSpanContext(context.active(), { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: 1, + }); + + const span = trace.getSpan(ctx)!; + + const data = getTraceData({ span }); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: + 'sentry-environment=production,sentry-public_key=username,sentry-trace_id=12345678901234567890123456789012,sentry-sampled=true', + }); + }); + + it('returns propagationContext DSC data if no span is available', () => { + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampled: true, + spanId: '1234567890123456', + dsc: { + environment: 'staging', + public_key: 'key', + trace_id: '12345678901234567890123456789012', + }, + }); + + const traceData = getTraceData(); + + expect(traceData).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012', + }); + }); + + it('works with an span with frozen DSC in traceState', () => { + const ctx = trace.setSpanContext(context.active(), { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: 1, + traceState: makeTraceState({ + dsc: { environment: 'test-dev', public_key: '456', trace_id: '12345678901234567890123456789088' }, + }), + }); + + context.with(ctx, () => { + const data = getTraceData(); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=test-dev,sentry-public_key=456,sentry-trace_id=12345678901234567890123456789088', + }); + }); + }); +}); diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index b209391f1dda..341470b0002a 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -197,6 +197,8 @@ function getNormalizedName( // If the route defined on the element is something like // Product} /> // We should check against the branch.pathname for the number of / separators + // TODO(v9): Put the implementation of `getNumberOfUrlSegments` in this file + // eslint-disable-next-line deprecation/deprecation getNumberOfUrlSegments(pathBuilder) !== getNumberOfUrlSegments(branch.pathname) && // We should not count wildcard operators in the url segments calculation pathBuilder.slice(-2) !== '/*' diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index d61f759dbf38..2e7dd3708806 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -17,6 +17,7 @@ export { addEventProcessor, addIntegration, addOpenTelemetryInstrumentation, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, amqplibIntegration, anrIntegration, @@ -43,6 +44,7 @@ export { endSession, expressErrorHandler, expressIntegration, + // eslint-disable-next-line deprecation/deprecation extractRequestData, extraErrorDataIntegration, fastifyIntegration, diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index ac1382bc5e00..fbd874a12df9 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -6,16 +6,16 @@ import { getActiveSpan, getClient, getRootSpan, + getTraceData, hasTracingEnabled, setHttpStatus, spanToJSON, - spanToTraceHeader, startSpan, winterCGRequestToRequestData, withIsolationScope, } from '@sentry/core'; -import { dynamicSamplingContextToSentryBaggageHeader, fill, isNodeEnv, loadModule, logger } from '@sentry/core'; -import { continueTrace, getDynamicSamplingContextFromSpan } from '@sentry/opentelemetry'; +import { fill, isNodeEnv, loadModule, logger } from '@sentry/core'; +import { continueTrace } from '@sentry/opentelemetry'; import type { RequestEventData, TransactionSource, WrappedFunction } from '@sentry/types'; import type { Span } from '@sentry/types'; @@ -204,18 +204,13 @@ function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string; } { - if (isNodeEnv() && hasTracingEnabled()) { - const span = getActiveSpan(); - const rootSpan = span && getRootSpan(span); + if (isNodeEnv()) { + const traceData = getTraceData(); - if (rootSpan) { - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(rootSpan); - - return { - sentryTrace: spanToTraceHeader(span), - sentryBaggage: dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext), - }; - } + return { + sentryTrace: traceData['sentry-trace'], + sentryBaggage: traceData.baggage, + }; } return {}; diff --git a/packages/remix/test/integration/test/client/root-loader.test.ts b/packages/remix/test/integration/test/client/root-loader.test.ts index 53b7648756e5..431195e8eab7 100644 --- a/packages/remix/test/integration/test/client/root-loader.test.ts +++ b/packages/remix/test/integration/test/client/root-loader.test.ts @@ -25,8 +25,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning an e const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -41,8 +41,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a pl const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -59,8 +59,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a `J const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -77,8 +77,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a de const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -93,8 +93,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning `nul const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -109,8 +109,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning `und const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -130,8 +130,8 @@ test('should inject `sentry-trace` and `baggage` into root loader throwing a red const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -151,8 +151,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a re const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index b2faa21768ba..d709a373e501 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -8,6 +8,7 @@ export { addEventProcessor, addIntegration, addOpenTelemetryInstrumentation, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, amqplibIntegration, anrIntegration, @@ -34,6 +35,7 @@ export { endSession, expressErrorHandler, expressIntegration, + // eslint-disable-next-line deprecation/deprecation extractRequestData, extraErrorDataIntegration, fastifyIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index c954d4b1bf78..4247dd46ff7a 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -8,6 +8,7 @@ export { addEventProcessor, addIntegration, addOpenTelemetryInstrumentation, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, amqplibIntegration, anrIntegration, @@ -34,6 +35,7 @@ export { endSession, expressErrorHandler, expressIntegration, + // eslint-disable-next-line deprecation/deprecation extractRequestData, extraErrorDataIntegration, fastifyIntegration, diff --git a/packages/types/src/severity.ts b/packages/types/src/severity.ts index 8a59bef56f30..73a685f5c5a6 100644 --- a/packages/types/src/severity.ts +++ b/packages/types/src/severity.ts @@ -1,3 +1 @@ -// Note: If this is ever changed, the `validSeverityLevels` array in `@sentry/core` needs to be changed, also. (See -// note there for why we can't derive one from the other.) export type SeverityLevel = 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug'; diff --git a/packages/utils/README.md b/packages/utils/README.md index 812bb86a3525..028fd82c4483 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -4,7 +4,11 @@

-# Sentry JavaScript SDK Utilities +# Sentry JavaScript SDK Utilities (DEPRECATED) + +> DEPRECATION NOTICE: The `@sentry/utils` package is deprecated. +> All exports have been moved to `@sentry/core`. +> Import everything from `@sentry/core` instead. [![npm version](https://img.shields.io/npm/v/@sentry/utils.svg)](https://www.npmjs.com/package/@sentry/utils) [![npm dm](https://img.shields.io/npm/dm/@sentry/utils.svg)](https://www.npmjs.com/package/@sentry/utils) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 0b4bcd669706..0a3a5a88f23f 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,537 +1,711 @@ -export { - applyAggregateErrorsToEvent, - getBreadcrumbLogLevelFromHttpStatusCode, - dsnFromString, - dsnToString, - makeDsn, - SentryError, - GLOBAL_OBJ, - getGlobalSingleton, - addConsoleInstrumentationHandler, - addFetchEndInstrumentationHandler, - addFetchInstrumentationHandler, - addGlobalErrorInstrumentationHandler, - addGlobalUnhandledRejectionInstrumentationHandler, - addHandler, - maybeInstrument, - resetInstrumentationHandlers, - triggerHandlers, - isDOMError, - isDOMException, - isElement, - isError, - isErrorEvent, - isEvent, - isInstanceOf, - isParameterizedString, - isPlainObject, - isPrimitive, - isRegExp, - isString, - isSyntheticEvent, - isThenable, - isVueViewModel, - isBrowser, - CONSOLE_LEVELS, - consoleSandbox, - logger, - originalConsoleMethods, - addContextToFrame, - addExceptionMechanism, - addExceptionTypeValue, - checkOrSetAlreadyCaught, - getEventDescription, - parseSemver, - uuid4, - normalize, - normalizeToSize, - addNonEnumerableProperty, - convertToPlainObject, - dropUndefinedKeys, - extractExceptionKeysForMessage, - fill, - getOriginalFunction, - markFunctionWrapped, - objectify, - makePromiseBuffer, - addNormalizedRequestDataToEvent, - winterCGHeadersToDict, - winterCGRequestToRequestData, - severityLevelFromString, - validSeverityLevels, - UNKNOWN_FUNCTION, - createStackParser, - getFramesFromEvent, - getFunctionName, - stackParserFromStackParserOptions, - stripSentryFramesAndReverse, - filenameIsInApp, - node, - nodeStackLineParser, - isMatchingPattern, - safeJoin, - snipLine, - stringMatchesSomePattern, - truncate, - SyncPromise, - rejectedSyncPromise, - resolvedSyncPromise, - dateTimestampInSeconds, - timestampInSeconds, - TRACEPARENT_REGEXP, - extractTraceparentData, - generateSentryTraceHeader, - propagationContextFromHeaders, - getSDKSource, - isBrowserBundle, - MAX_BAGGAGE_STRING_LENGTH, - SENTRY_BAGGAGE_KEY_PREFIX, - SENTRY_BAGGAGE_KEY_PREFIX_REGEX, - baggageHeaderToDynamicSamplingContext, - dynamicSamplingContextToSentryBaggageHeader, - parseBaggageHeader, - addItemToEnvelope, - createAttachmentEnvelopeItem, - createEnvelope, - createEventEnvelopeHeaders, - createSpanEnvelopeItem, - envelopeContainsItemType, - envelopeItemTypeToDataCategory, - forEachEnvelopeItem, - getSdkMetadataForEnvelopeHeader, - parseEnvelope, - serializeEnvelope, - createClientReportEnvelope, - DEFAULT_RETRY_AFTER, - disabledUntil, - isRateLimited, - parseRetryAfterHeader, - updateRateLimits, - eventFromMessage, - eventFromUnknownInput, - exceptionFromError, - parseStackFrames, - callFrameToStackFrame, - watchdogTimer, - LRUMap, - generatePropagationContext, - vercelWaitUntil, - SDK_VERSION, - getDebugImagesForResources, - getFilenameToDebugIdMap, - escapeStringForRegex, - basename, - dirname, - isAbsolute, - join, - normalizePath, - relative, - resolve, - getComponentName, - getDomElement, - getLocationHref, - htmlTreeAsString, - isNativeFunction, - supportsDOMError, - supportsDOMException, - supportsErrorEvent, - supportsFetch, - supportsNativeFetch, - supportsReferrerPolicy, - supportsReportingObserver, - _browserPerformanceTimeOriginMode, - browserPerformanceTimeOrigin, - supportsHistory, - dynamicRequire, - isNodeEnv, - loadModule, - flatten, - memoBuilder, - arrayify, - normalizeUrlToBase, - urlEncode, - // eslint-disable-next-line deprecation/deprecation - extractPathForTransaction, - DEFAULT_USER_INCLUDES, - extractRequestData, - addRequestDataToEvent, - _asyncNullishCoalesce, - _asyncOptionalChain, - _asyncOptionalChainDelete, - _nullishCoalesce, - _optionalChain, - _optionalChainDelete, - BAGGAGE_HEADER_NAME, - getNumberOfUrlSegments, - getSanitizedUrlString, - parseUrl, - stripUrlQueryAndFragment, - makeFifoCache, +/* eslint-disable max-lines */ +import { + BAGGAGE_HEADER_NAME as BAGGAGE_HEADER_NAME_imported, + CONSOLE_LEVELS as CONSOLE_LEVELS_imported, + DEFAULT_RETRY_AFTER as DEFAULT_RETRY_AFTER_imported, + DEFAULT_USER_INCLUDES as DEFAULT_USER_INCLUDES_imported, + GLOBAL_OBJ as GLOBAL_OBJ_imported, + LRUMap as LRUMap_imported, + MAX_BAGGAGE_STRING_LENGTH as MAX_BAGGAGE_STRING_LENGTH_imported, + SDK_VERSION as SDK_VERSION_imported, + SENTRY_BAGGAGE_KEY_PREFIX as SENTRY_BAGGAGE_KEY_PREFIX_imported, + SENTRY_BAGGAGE_KEY_PREFIX_REGEX as SENTRY_BAGGAGE_KEY_PREFIX_REGEX_imported, + SentryError as SentryError_imported, + SyncPromise as SyncPromise_imported, + TRACEPARENT_REGEXP as TRACEPARENT_REGEXP_imported, + UNKNOWN_FUNCTION as UNKNOWN_FUNCTION_imported, + _asyncNullishCoalesce as _asyncNullishCoalesce_imported, + _asyncOptionalChain as _asyncOptionalChain_imported, + _asyncOptionalChainDelete as _asyncOptionalChainDelete_imported, + _browserPerformanceTimeOriginMode as _browserPerformanceTimeOriginMode_imported, + _nullishCoalesce as _nullishCoalesce_imported, + _optionalChain as _optionalChain_imported, + _optionalChainDelete as _optionalChainDelete_imported, + addConsoleInstrumentationHandler as addConsoleInstrumentationHandler_imported, + addContextToFrame as addContextToFrame_imported, + addExceptionMechanism as addExceptionMechanism_imported, + addExceptionTypeValue as addExceptionTypeValue_imported, + addFetchEndInstrumentationHandler as addFetchEndInstrumentationHandler_imported, + addFetchInstrumentationHandler as addFetchInstrumentationHandler_imported, + addGlobalErrorInstrumentationHandler as addGlobalErrorInstrumentationHandler_imported, + addGlobalUnhandledRejectionInstrumentationHandler as addGlobalUnhandledRejectionInstrumentationHandler_imported, + addHandler as addHandler_imported, + addItemToEnvelope as addItemToEnvelope_imported, + addNonEnumerableProperty as addNonEnumerableProperty_imported, + addNormalizedRequestDataToEvent as addNormalizedRequestDataToEvent_imported, + addRequestDataToEvent as addRequestDataToEvent_imported, + applyAggregateErrorsToEvent as applyAggregateErrorsToEvent_imported, + arrayify as arrayify_imported, + baggageHeaderToDynamicSamplingContext as baggageHeaderToDynamicSamplingContext_imported, + basename as basename_imported, + browserPerformanceTimeOrigin as browserPerformanceTimeOrigin_imported, + callFrameToStackFrame as callFrameToStackFrame_imported, + checkOrSetAlreadyCaught as checkOrSetAlreadyCaught_imported, + consoleSandbox as consoleSandbox_imported, + convertToPlainObject as convertToPlainObject_imported, + createAttachmentEnvelopeItem as createAttachmentEnvelopeItem_imported, + createClientReportEnvelope as createClientReportEnvelope_imported, + createEnvelope as createEnvelope_imported, + createEventEnvelopeHeaders as createEventEnvelopeHeaders_imported, + createSpanEnvelopeItem as createSpanEnvelopeItem_imported, + createStackParser as createStackParser_imported, + dateTimestampInSeconds as dateTimestampInSeconds_imported, + dirname as dirname_imported, + disabledUntil as disabledUntil_imported, + dropUndefinedKeys as dropUndefinedKeys_imported, + dsnFromString as dsnFromString_imported, + dsnToString as dsnToString_imported, + dynamicRequire as dynamicRequire_imported, + dynamicSamplingContextToSentryBaggageHeader as dynamicSamplingContextToSentryBaggageHeader_imported, + envelopeContainsItemType as envelopeContainsItemType_imported, + envelopeItemTypeToDataCategory as envelopeItemTypeToDataCategory_imported, + escapeStringForRegex as escapeStringForRegex_imported, + eventFromMessage as eventFromMessage_imported, + eventFromUnknownInput as eventFromUnknownInput_imported, + exceptionFromError as exceptionFromError_imported, + extractExceptionKeysForMessage as extractExceptionKeysForMessage_imported, + extractPathForTransaction as extractPathForTransaction_imported, + extractRequestData as extractRequestData_imported, + extractTraceparentData as extractTraceparentData_imported, + filenameIsInApp as filenameIsInApp_imported, + fill as fill_imported, + flatten as flatten_imported, + forEachEnvelopeItem as forEachEnvelopeItem_imported, + generatePropagationContext as generatePropagationContext_imported, + generateSentryTraceHeader as generateSentryTraceHeader_imported, + getBreadcrumbLogLevelFromHttpStatusCode as getBreadcrumbLogLevelFromHttpStatusCode_imported, + getComponentName as getComponentName_imported, + getDebugImagesForResources as getDebugImagesForResources_imported, + getDomElement as getDomElement_imported, + getEventDescription as getEventDescription_imported, + getFilenameToDebugIdMap as getFilenameToDebugIdMap_imported, + getFramesFromEvent as getFramesFromEvent_imported, + getFunctionName as getFunctionName_imported, + getGlobalSingleton as getGlobalSingleton_imported, + getLocationHref as getLocationHref_imported, + getNumberOfUrlSegments as getNumberOfUrlSegments_imported, + getOriginalFunction as getOriginalFunction_imported, + getSDKSource as getSDKSource_imported, + getSanitizedUrlString as getSanitizedUrlString_imported, + getSdkMetadataForEnvelopeHeader as getSdkMetadataForEnvelopeHeader_imported, + htmlTreeAsString as htmlTreeAsString_imported, + isAbsolute as isAbsolute_imported, + isBrowser as isBrowser_imported, + isBrowserBundle as isBrowserBundle_imported, + isDOMError as isDOMError_imported, + isDOMException as isDOMException_imported, + isElement as isElement_imported, + isError as isError_imported, + isErrorEvent as isErrorEvent_imported, + isEvent as isEvent_imported, + isInstanceOf as isInstanceOf_imported, + isMatchingPattern as isMatchingPattern_imported, + isNativeFunction as isNativeFunction_imported, + isNodeEnv as isNodeEnv_imported, + isParameterizedString as isParameterizedString_imported, + isPlainObject as isPlainObject_imported, + isPrimitive as isPrimitive_imported, + isRateLimited as isRateLimited_imported, + isRegExp as isRegExp_imported, + isString as isString_imported, + isSyntheticEvent as isSyntheticEvent_imported, + isThenable as isThenable_imported, + isVueViewModel as isVueViewModel_imported, + join as join_imported, + loadModule as loadModule_imported, + logger as logger_imported, + makeDsn as makeDsn_imported, + makeFifoCache as makeFifoCache_imported, + makePromiseBuffer as makePromiseBuffer_imported, + markFunctionWrapped as markFunctionWrapped_imported, + maybeInstrument as maybeInstrument_imported, + memoBuilder as memoBuilder_imported, + node as node_imported, + nodeStackLineParser as nodeStackLineParser_imported, + normalize as normalize_imported, + normalizePath as normalizePath_imported, + normalizeToSize as normalizeToSize_imported, + normalizeUrlToBase as normalizeUrlToBase_imported, + objectify as objectify_imported, + originalConsoleMethods as originalConsoleMethods_imported, + parseBaggageHeader as parseBaggageHeader_imported, + parseEnvelope as parseEnvelope_imported, + parseRetryAfterHeader as parseRetryAfterHeader_imported, + parseSemver as parseSemver_imported, + parseStackFrames as parseStackFrames_imported, + parseUrl as parseUrl_imported, + propagationContextFromHeaders as propagationContextFromHeaders_imported, + rejectedSyncPromise as rejectedSyncPromise_imported, + relative as relative_imported, + resetInstrumentationHandlers as resetInstrumentationHandlers_imported, + resolve as resolve_imported, + resolvedSyncPromise as resolvedSyncPromise_imported, + safeJoin as safeJoin_imported, + serializeEnvelope as serializeEnvelope_imported, + severityLevelFromString as severityLevelFromString_imported, + snipLine as snipLine_imported, + stackParserFromStackParserOptions as stackParserFromStackParserOptions_imported, + stringMatchesSomePattern as stringMatchesSomePattern_imported, + stripSentryFramesAndReverse as stripSentryFramesAndReverse_imported, + stripUrlQueryAndFragment as stripUrlQueryAndFragment_imported, + supportsDOMError as supportsDOMError_imported, + supportsDOMException as supportsDOMException_imported, + supportsErrorEvent as supportsErrorEvent_imported, + supportsFetch as supportsFetch_imported, + supportsHistory as supportsHistory_imported, + supportsNativeFetch as supportsNativeFetch_imported, + supportsReferrerPolicy as supportsReferrerPolicy_imported, + supportsReportingObserver as supportsReportingObserver_imported, + timestampInSeconds as timestampInSeconds_imported, + triggerHandlers as triggerHandlers_imported, + truncate as truncate_imported, + updateRateLimits as updateRateLimits_imported, + urlEncode as urlEncode_imported, + uuid4 as uuid4_imported, + validSeverityLevels as validSeverityLevels_imported, + vercelWaitUntil as vercelWaitUntil_imported, + watchdogTimer as watchdogTimer_imported, + winterCGHeadersToDict as winterCGHeadersToDict_imported, + winterCGRequestToRequestData as winterCGRequestToRequestData_imported, } from '@sentry/core'; -export type { - InternalGlobal, - PromiseBuffer, - RateLimits, - AddRequestDataToEventOptions, - SdkSource, - // eslint-disable-next-line deprecation/deprecation - TransactionNamingScheme, +/** @deprecated Import from `@sentry/core` instead. */ +export const applyAggregateErrorsToEvent = applyAggregateErrorsToEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getBreadcrumbLogLevelFromHttpStatusCode = getBreadcrumbLogLevelFromHttpStatusCode_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dsnFromString = dsnFromString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dsnToString = dsnToString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const makeDsn = makeDsn_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const SentryError = SentryError_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const GLOBAL_OBJ = GLOBAL_OBJ_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getGlobalSingleton = getGlobalSingleton_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addConsoleInstrumentationHandler = addConsoleInstrumentationHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addFetchEndInstrumentationHandler = addFetchEndInstrumentationHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addFetchInstrumentationHandler = addFetchInstrumentationHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addGlobalErrorInstrumentationHandler = addGlobalErrorInstrumentationHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addGlobalUnhandledRejectionInstrumentationHandler = + addGlobalUnhandledRejectionInstrumentationHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addHandler = addHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const maybeInstrument = maybeInstrument_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const resetInstrumentationHandlers = resetInstrumentationHandlers_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const triggerHandlers = triggerHandlers_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isDOMError = isDOMError_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isDOMException = isDOMException_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isElement = isElement_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isError = isError_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isErrorEvent = isErrorEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isEvent = isEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isInstanceOf = isInstanceOf_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isParameterizedString = isParameterizedString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isPlainObject = isPlainObject_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isPrimitive = isPrimitive_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isRegExp = isRegExp_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isString = isString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isSyntheticEvent = isSyntheticEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isThenable = isThenable_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isVueViewModel = isVueViewModel_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isBrowser = isBrowser_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const CONSOLE_LEVELS = CONSOLE_LEVELS_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const consoleSandbox = consoleSandbox_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const logger = logger_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const originalConsoleMethods = originalConsoleMethods_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addContextToFrame = addContextToFrame_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addExceptionMechanism = addExceptionMechanism_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addExceptionTypeValue = addExceptionTypeValue_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const checkOrSetAlreadyCaught = checkOrSetAlreadyCaught_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getEventDescription = getEventDescription_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseSemver = parseSemver_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const uuid4 = uuid4_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const normalize = normalize_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const normalizeToSize = normalizeToSize_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addNonEnumerableProperty = addNonEnumerableProperty_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const convertToPlainObject = convertToPlainObject_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dropUndefinedKeys = dropUndefinedKeys_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const extractExceptionKeysForMessage = extractExceptionKeysForMessage_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const fill = fill_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getOriginalFunction = getOriginalFunction_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const markFunctionWrapped = markFunctionWrapped_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const objectify = objectify_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const makePromiseBuffer = makePromiseBuffer_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addNormalizedRequestDataToEvent = addNormalizedRequestDataToEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const winterCGHeadersToDict = winterCGHeadersToDict_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const winterCGRequestToRequestData = winterCGRequestToRequestData_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const severityLevelFromString = severityLevelFromString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const validSeverityLevels = validSeverityLevels_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const UNKNOWN_FUNCTION = UNKNOWN_FUNCTION_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createStackParser = createStackParser_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getFramesFromEvent = getFramesFromEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getFunctionName = getFunctionName_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const stackParserFromStackParserOptions = stackParserFromStackParserOptions_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const stripSentryFramesAndReverse = stripSentryFramesAndReverse_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const filenameIsInApp = filenameIsInApp_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const node = node_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const nodeStackLineParser = nodeStackLineParser_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isMatchingPattern = isMatchingPattern_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const safeJoin = safeJoin_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const snipLine = snipLine_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const stringMatchesSomePattern = stringMatchesSomePattern_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const truncate = truncate_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const SyncPromise = SyncPromise_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const rejectedSyncPromise = rejectedSyncPromise_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const resolvedSyncPromise = resolvedSyncPromise_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dateTimestampInSeconds = dateTimestampInSeconds_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const timestampInSeconds = timestampInSeconds_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const TRACEPARENT_REGEXP = TRACEPARENT_REGEXP_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const extractTraceparentData = extractTraceparentData_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const generateSentryTraceHeader = generateSentryTraceHeader_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const propagationContextFromHeaders = propagationContextFromHeaders_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getSDKSource = getSDKSource_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isBrowserBundle = isBrowserBundle_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const MAX_BAGGAGE_STRING_LENGTH = MAX_BAGGAGE_STRING_LENGTH_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const SENTRY_BAGGAGE_KEY_PREFIX = SENTRY_BAGGAGE_KEY_PREFIX_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const SENTRY_BAGGAGE_KEY_PREFIX_REGEX = SENTRY_BAGGAGE_KEY_PREFIX_REGEX_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const baggageHeaderToDynamicSamplingContext = baggageHeaderToDynamicSamplingContext_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dynamicSamplingContextToSentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseBaggageHeader = parseBaggageHeader_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addItemToEnvelope = addItemToEnvelope_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createAttachmentEnvelopeItem = createAttachmentEnvelopeItem_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createEnvelope = createEnvelope_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createEventEnvelopeHeaders = createEventEnvelopeHeaders_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createSpanEnvelopeItem = createSpanEnvelopeItem_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const envelopeContainsItemType = envelopeContainsItemType_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const envelopeItemTypeToDataCategory = envelopeItemTypeToDataCategory_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const forEachEnvelopeItem = forEachEnvelopeItem_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getSdkMetadataForEnvelopeHeader = getSdkMetadataForEnvelopeHeader_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseEnvelope = parseEnvelope_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const serializeEnvelope = serializeEnvelope_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createClientReportEnvelope = createClientReportEnvelope_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const DEFAULT_RETRY_AFTER = DEFAULT_RETRY_AFTER_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const disabledUntil = disabledUntil_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isRateLimited = isRateLimited_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseRetryAfterHeader = parseRetryAfterHeader_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const updateRateLimits = updateRateLimits_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const eventFromMessage = eventFromMessage_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const eventFromUnknownInput = eventFromUnknownInput_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const exceptionFromError = exceptionFromError_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseStackFrames = parseStackFrames_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const callFrameToStackFrame = callFrameToStackFrame_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const watchdogTimer = watchdogTimer_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const LRUMap = LRUMap_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const generatePropagationContext = generatePropagationContext_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const vercelWaitUntil = vercelWaitUntil_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const SDK_VERSION = SDK_VERSION_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getDebugImagesForResources = getDebugImagesForResources_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getFilenameToDebugIdMap = getFilenameToDebugIdMap_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const escapeStringForRegex = escapeStringForRegex_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const basename = basename_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dirname = dirname_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isAbsolute = isAbsolute_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const join = join_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const normalizePath = normalizePath_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const relative = relative_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const resolve = resolve_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getComponentName = getComponentName_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getDomElement = getDomElement_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getLocationHref = getLocationHref_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const htmlTreeAsString = htmlTreeAsString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isNativeFunction = isNativeFunction_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsDOMError = supportsDOMError_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsDOMException = supportsDOMException_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsErrorEvent = supportsErrorEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsFetch = supportsFetch_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsNativeFetch = supportsNativeFetch_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsReferrerPolicy = supportsReferrerPolicy_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsReportingObserver = supportsReportingObserver_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _browserPerformanceTimeOriginMode = _browserPerformanceTimeOriginMode_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const browserPerformanceTimeOrigin = browserPerformanceTimeOrigin_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsHistory = supportsHistory_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dynamicRequire = dynamicRequire_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isNodeEnv = isNodeEnv_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const loadModule = loadModule_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const flatten = flatten_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const memoBuilder = memoBuilder_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const arrayify = arrayify_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const normalizeUrlToBase = normalizeUrlToBase_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const urlEncode = urlEncode_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const extractPathForTransaction = extractPathForTransaction_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const DEFAULT_USER_INCLUDES = DEFAULT_USER_INCLUDES_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const extractRequestData = extractRequestData_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const addRequestDataToEvent = addRequestDataToEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _asyncNullishCoalesce = _asyncNullishCoalesce_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _asyncOptionalChain = _asyncOptionalChain_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _asyncOptionalChainDelete = _asyncOptionalChainDelete_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _nullishCoalesce = _nullishCoalesce_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _optionalChain = _optionalChain_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _optionalChainDelete = _optionalChainDelete_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const BAGGAGE_HEADER_NAME = BAGGAGE_HEADER_NAME_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const getNumberOfUrlSegments = getNumberOfUrlSegments_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getSanitizedUrlString = getSanitizedUrlString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseUrl = parseUrl_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const stripUrlQueryAndFragment = stripUrlQueryAndFragment_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const makeFifoCache = makeFifoCache_imported; + +import type { + AddRequestDataToEventOptions as AddRequestDataToEventOptions_imported, + InternalGlobal as InternalGlobal_imported, + PromiseBuffer as PromiseBuffer_imported, + RateLimits as RateLimits_imported, + SdkSource as SdkSource_imported, + TransactionNamingScheme as TransactionNamingScheme_imported, } from '@sentry/core'; -// TODO(v9/lforst): Uncomment below to add deprecation notices - -// import { -// applyAggregateErrorsToEvent as applyAggregateErrorsToEvent_imported, -// getBreadcrumbLogLevelFromHttpStatusCode as getBreadcrumbLogLevelFromHttpStatusCode_imported, -// dsnFromString as dsnFromString_imported, -// dsnToString as dsnToString_imported, -// makeDsn as makeDsn_imported, -// SentryError as SentryError_imported, -// GLOBAL_OBJ as GLOBAL_OBJ_imported, -// getGlobalSingleton as getGlobalSingleton_imported, -// addConsoleInstrumentationHandler as addConsoleInstrumentationHandler_imported, -// addFetchEndInstrumentationHandler as addFetchEndInstrumentationHandler_imported, -// addFetchInstrumentationHandler as addFetchInstrumentationHandler_imported, -// addGlobalErrorInstrumentationHandler as addGlobalErrorInstrumentationHandler_imported, -// addGlobalUnhandledRejectionInstrumentationHandler as addGlobalUnhandledRejectionInstrumentationHandler_imported, -// addHandler as addHandler_imported, -// maybeInstrument as maybeInstrument_imported, -// resetInstrumentationHandlers as resetInstrumentationHandlers_imported, -// triggerHandlers as triggerHandlers_imported, -// isDOMError as isDOMError_imported, -// isDOMException as isDOMException_imported, -// isElement as isElement_imported, -// isError as isError_imported, -// isErrorEvent as isErrorEvent_imported, -// isEvent as isEvent_imported, -// isInstanceOf as isInstanceOf_imported, -// isParameterizedString as isParameterizedString_imported, -// isPlainObject as isPlainObject_imported, -// isPrimitive as isPrimitive_imported, -// isRegExp as isRegExp_imported, -// isString as isString_imported, -// isSyntheticEvent as isSyntheticEvent_imported, -// isThenable as isThenable_imported, -// isVueViewModel as isVueViewModel_imported, -// isBrowser as isBrowser_imported, -// CONSOLE_LEVELS as CONSOLE_LEVELS_imported, -// consoleSandbox as consoleSandbox_imported, -// logger as logger_imported, -// originalConsoleMethods as originalConsoleMethods_imported, -// addContextToFrame as addContextToFrame_imported, -// addExceptionMechanism as addExceptionMechanism_imported, -// addExceptionTypeValue as addExceptionTypeValue_imported, -// checkOrSetAlreadyCaught as checkOrSetAlreadyCaught_imported, -// getEventDescription as getEventDescription_imported, -// parseSemver as parseSemver_imported, -// uuid4 as uuid4_imported, -// normalize as normalize_imported, -// normalizeToSize as normalizeToSize_imported, -// addNonEnumerableProperty as addNonEnumerableProperty_imported, -// convertToPlainObject as convertToPlainObject_imported, -// dropUndefinedKeys as dropUndefinedKeys_imported, -// extractExceptionKeysForMessage as extractExceptionKeysForMessage_imported, -// fill as fill_imported, -// getOriginalFunction as getOriginalFunction_imported, -// markFunctionWrapped as markFunctionWrapped_imported, -// objectify as objectify_imported, -// makePromiseBuffer as makePromiseBuffer_imported, -// addNormalizedRequestDataToEvent as addNormalizedRequestDataToEvent_imported, -// winterCGHeadersToDict as winterCGHeadersToDict_imported, -// winterCGRequestToRequestData as winterCGRequestToRequestData_imported, -// severityLevelFromString as severityLevelFromString_imported, -// validSeverityLevels as validSeverityLevels_imported, -// UNKNOWN_FUNCTION as UNKNOWN_FUNCTION_imported, -// createStackParser as createStackParser_imported, -// getFramesFromEvent as getFramesFromEvent_imported, -// getFunctionName as getFunctionName_imported, -// stackParserFromStackParserOptions as stackParserFromStackParserOptions_imported, -// stripSentryFramesAndReverse as stripSentryFramesAndReverse_imported, -// filenameIsInApp as filenameIsInApp_imported, -// node as node_imported, -// nodeStackLineParser as nodeStackLineParser_imported, -// isMatchingPattern as isMatchingPattern_imported, -// safeJoin as safeJoin_imported, -// snipLine as snipLine_imported, -// stringMatchesSomePattern as stringMatchesSomePattern_imported, -// truncate as truncate_imported, -// SyncPromise as SyncPromise_imported, -// rejectedSyncPromise as rejectedSyncPromise_imported, -// resolvedSyncPromise as resolvedSyncPromise_imported, -// dateTimestampInSeconds as dateTimestampInSeconds_imported, -// timestampInSeconds as timestampInSeconds_imported, -// TRACEPARENT_REGEXP as TRACEPARENT_REGEXP_imported, -// extractTraceparentData as extractTraceparentData_imported, -// generateSentryTraceHeader as generateSentryTraceHeader_imported, -// propagationContextFromHeaders as propagationContextFromHeaders_imported, -// getSDKSource as getSDKSource_imported, -// isBrowserBundle as isBrowserBundle_imported, -// MAX_BAGGAGE_STRING_LENGTH as MAX_BAGGAGE_STRING_LENGTH_imported, -// SENTRY_BAGGAGE_KEY_PREFIX as SENTRY_BAGGAGE_KEY_PREFIX_imported, -// SENTRY_BAGGAGE_KEY_PREFIX_REGEX as SENTRY_BAGGAGE_KEY_PREFIX_REGEX_imported, -// baggageHeaderToDynamicSamplingContext as baggageHeaderToDynamicSamplingContext_imported, -// dynamicSamplingContextToSentryBaggageHeader as dynamicSamplingContextToSentryBaggageHeader_imported, -// parseBaggageHeader as parseBaggageHeader_imported, -// addItemToEnvelope as addItemToEnvelope_imported, -// createAttachmentEnvelopeItem as createAttachmentEnvelopeItem_imported, -// createEnvelope as createEnvelope_imported, -// createEventEnvelopeHeaders as createEventEnvelopeHeaders_imported, -// createSpanEnvelopeItem as createSpanEnvelopeItem_imported, -// envelopeContainsItemType as envelopeContainsItemType_imported, -// envelopeItemTypeToDataCategory as envelopeItemTypeToDataCategory_imported, -// forEachEnvelopeItem as forEachEnvelopeItem_imported, -// getSdkMetadataForEnvelopeHeader as getSdkMetadataForEnvelopeHeader_imported, -// parseEnvelope as parseEnvelope_imported, -// serializeEnvelope as serializeEnvelope_imported, -// createClientReportEnvelope as createClientReportEnvelope_imported, -// DEFAULT_RETRY_AFTER as DEFAULT_RETRY_AFTER_imported, -// disabledUntil as disabledUntil_imported, -// isRateLimited as isRateLimited_imported, -// parseRetryAfterHeader as parseRetryAfterHeader_imported, -// updateRateLimits as updateRateLimits_imported, -// eventFromMessage as eventFromMessage_imported, -// eventFromUnknownInput as eventFromUnknownInput_imported, -// exceptionFromError as exceptionFromError_imported, -// parseStackFrames as parseStackFrames_imported, -// callFrameToStackFrame as callFrameToStackFrame_imported, -// watchdogTimer as watchdogTimer_imported, -// LRUMap as LRUMap_imported, -// generatePropagationContext as generatePropagationContext_imported, -// vercelWaitUntil as vercelWaitUntil_imported, -// SDK_VERSION as SDK_VERSION_imported, -// getDebugImagesForResources as getDebugImagesForResources_imported, -// getFilenameToDebugIdMap as getFilenameToDebugIdMap_imported, -// escapeStringForRegex as escapeStringForRegex_imported, -// basename as basename_imported, -// dirname as dirname_imported, -// isAbsolute as isAbsolute_imported, -// join as join_imported, -// normalizePath as normalizePath_imported, -// relative as relative_imported, -// resolve as resolve_imported, -// getComponentName as getComponentName_imported, -// getDomElement as getDomElement_imported, -// getLocationHref as getLocationHref_imported, -// htmlTreeAsString as htmlTreeAsString_imported, -// isNativeFunction as isNativeFunction_imported, -// supportsDOMError as supportsDOMError_imported, -// supportsDOMException as supportsDOMException_imported, -// supportsErrorEvent as supportsErrorEvent_imported, -// supportsFetch as supportsFetch_imported, -// supportsNativeFetch as supportsNativeFetch_imported, -// supportsReferrerPolicy as supportsReferrerPolicy_imported, -// supportsReportingObserver as supportsReportingObserver_imported, -// _browserPerformanceTimeOriginMode as _browserPerformanceTimeOriginMode_imported, -// browserPerformanceTimeOrigin as browserPerformanceTimeOrigin_imported, -// supportsHistory as supportsHistory_imported, -// dynamicRequire as dynamicRequire_imported, -// isNodeEnv as isNodeEnv_imported, -// loadModule as loadModule_imported, -// flatten as flatten_imported, -// memoBuilder as memoBuilder_imported, -// arrayify as arrayify_imported, -// normalizeUrlToBase as normalizeUrlToBase_imported, -// urlEncode as urlEncode_imported, -// extractPathForTransaction as extractPathForTransaction_imported, -// DEFAULT_USER_INCLUDES as DEFAULT_USER_INCLUDES_imported, -// extractRequestData as extractRequestData_imported, -// addRequestDataToEvent as addRequestDataToEvent_imported, -// _asyncNullishCoalesce as _asyncNullishCoalesce_imported, -// _asyncOptionalChain as _asyncOptionalChain_imported, -// _asyncOptionalChainDelete as _asyncOptionalChainDelete_imported, -// _nullishCoalesce as _nullishCoalesce_imported, -// _optionalChain as _optionalChain_imported, -// _optionalChainDelete as _optionalChainDelete_imported, -// BAGGAGE_HEADER_NAME as BAGGAGE_HEADER_NAME_imported, -// getNumberOfUrlSegments as getNumberOfUrlSegments_imported, -// getSanitizedUrlString as getSanitizedUrlString_imported, -// parseUrl as parseUrl_imported, -// stripUrlQueryAndFragment as stripUrlQueryAndFragment_imported, -// makeFifoCache as makeFifoCache_imported, -// } from '@sentry/core'; - -// export const applyAggregateErrorsToEvent = applyAggregateErrorsToEvent_imported; -// export const getBreadcrumbLogLevelFromHttpStatusCode = getBreadcrumbLogLevelFromHttpStatusCode_imported; -// export const dsnFromString = dsnFromString_imported; -// export const dsnToString = dsnToString_imported; -// export const makeDsn = makeDsn_imported; -// export const SentryError = SentryError_imported; -// export const GLOBAL_OBJ = GLOBAL_OBJ_imported; -// export const getGlobalSingleton = getGlobalSingleton_imported; -// export const addConsoleInstrumentationHandler = addConsoleInstrumentationHandler_imported; -// export const addFetchEndInstrumentationHandler = addFetchEndInstrumentationHandler_imported; -// export const addFetchInstrumentationHandler = addFetchInstrumentationHandler_imported; -// export const addGlobalErrorInstrumentationHandler = addGlobalErrorInstrumentationHandler_imported; -// export const addGlobalUnhandledRejectionInstrumentationHandler = -// addGlobalUnhandledRejectionInstrumentationHandler_imported; -// export const addHandler = addHandler_imported; -// export const maybeInstrument = maybeInstrument_imported; -// export const resetInstrumentationHandlers = resetInstrumentationHandlers_imported; -// export const triggerHandlers = triggerHandlers_imported; -// export const isDOMError = isDOMError_imported; -// export const isDOMException = isDOMException_imported; -// export const isElement = isElement_imported; -// export const isError = isError_imported; -// export const isErrorEvent = isErrorEvent_imported; -// export const isEvent = isEvent_imported; -// export const isInstanceOf = isInstanceOf_imported; -// export const isParameterizedString = isParameterizedString_imported; -// export const isPlainObject = isPlainObject_imported; -// export const isPrimitive = isPrimitive_imported; -// export const isRegExp = isRegExp_imported; -// export const isString = isString_imported; -// export const isSyntheticEvent = isSyntheticEvent_imported; -// export const isThenable = isThenable_imported; -// export const isVueViewModel = isVueViewModel_imported; -// export const isBrowser = isBrowser_imported; -// export const CONSOLE_LEVELS = CONSOLE_LEVELS_imported; -// export const consoleSandbox = consoleSandbox_imported; -// export const logger = logger_imported; -// export const originalConsoleMethods = originalConsoleMethods_imported; -// export const addContextToFrame = addContextToFrame_imported; -// export const addExceptionMechanism = addExceptionMechanism_imported; -// export const addExceptionTypeValue = addExceptionTypeValue_imported; -// export const checkOrSetAlreadyCaught = checkOrSetAlreadyCaught_imported; -// export const getEventDescription = getEventDescription_imported; -// export const parseSemver = parseSemver_imported; -// export const uuid4 = uuid4_imported; -// export const normalize = normalize_imported; -// export const normalizeToSize = normalizeToSize_imported; -// export const addNonEnumerableProperty = addNonEnumerableProperty_imported; -// export const convertToPlainObject = convertToPlainObject_imported; -// export const dropUndefinedKeys = dropUndefinedKeys_imported; -// export const extractExceptionKeysForMessage = extractExceptionKeysForMessage_imported; -// export const fill = fill_imported; -// export const getOriginalFunction = getOriginalFunction_imported; -// export const markFunctionWrapped = markFunctionWrapped_imported; -// export const objectify = objectify_imported; -// export const makePromiseBuffer = makePromiseBuffer_imported; -// export const addNormalizedRequestDataToEvent = addNormalizedRequestDataToEvent_imported; -// export const winterCGHeadersToDict = winterCGHeadersToDict_imported; -// export const winterCGRequestToRequestData = winterCGRequestToRequestData_imported; -// export const severityLevelFromString = severityLevelFromString_imported; -// export const validSeverityLevels = validSeverityLevels_imported; -// export const UNKNOWN_FUNCTION = UNKNOWN_FUNCTION_imported; -// export const createStackParser = createStackParser_imported; -// export const getFramesFromEvent = getFramesFromEvent_imported; -// export const getFunctionName = getFunctionName_imported; -// export const stackParserFromStackParserOptions = stackParserFromStackParserOptions_imported; -// export const stripSentryFramesAndReverse = stripSentryFramesAndReverse_imported; -// export const filenameIsInApp = filenameIsInApp_imported; -// export const node = node_imported; -// export const nodeStackLineParser = nodeStackLineParser_imported; -// export const isMatchingPattern = isMatchingPattern_imported; -// export const safeJoin = safeJoin_imported; -// export const snipLine = snipLine_imported; -// export const stringMatchesSomePattern = stringMatchesSomePattern_imported; -// export const truncate = truncate_imported; -// export const SyncPromise = SyncPromise_imported; -// export const rejectedSyncPromise = rejectedSyncPromise_imported; -// export const resolvedSyncPromise = resolvedSyncPromise_imported; -// export const dateTimestampInSeconds = dateTimestampInSeconds_imported; -// export const timestampInSeconds = timestampInSeconds_imported; -// export const TRACEPARENT_REGEXP = TRACEPARENT_REGEXP_imported; -// export const extractTraceparentData = extractTraceparentData_imported; -// export const generateSentryTraceHeader = generateSentryTraceHeader_imported; -// export const propagationContextFromHeaders = propagationContextFromHeaders_imported; -// export const getSDKSource = getSDKSource_imported; -// export const isBrowserBundle = isBrowserBundle_imported; -// export const MAX_BAGGAGE_STRING_LENGTH = MAX_BAGGAGE_STRING_LENGTH_imported; -// export const SENTRY_BAGGAGE_KEY_PREFIX = SENTRY_BAGGAGE_KEY_PREFIX_imported; -// export const SENTRY_BAGGAGE_KEY_PREFIX_REGEX = SENTRY_BAGGAGE_KEY_PREFIX_REGEX_imported; -// export const baggageHeaderToDynamicSamplingContext = baggageHeaderToDynamicSamplingContext_imported; -// export const dynamicSamplingContextToSentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader_imported; -// export const parseBaggageHeader = parseBaggageHeader_imported; -// export const addItemToEnvelope = addItemToEnvelope_imported; -// export const createAttachmentEnvelopeItem = createAttachmentEnvelopeItem_imported; -// export const createEnvelope = createEnvelope_imported; -// export const createEventEnvelopeHeaders = createEventEnvelopeHeaders_imported; -// export const createSpanEnvelopeItem = createSpanEnvelopeItem_imported; -// export const envelopeContainsItemType = envelopeContainsItemType_imported; -// export const envelopeItemTypeToDataCategory = envelopeItemTypeToDataCategory_imported; -// export const forEachEnvelopeItem = forEachEnvelopeItem_imported; -// export const getSdkMetadataForEnvelopeHeader = getSdkMetadataForEnvelopeHeader_imported; -// export const parseEnvelope = parseEnvelope_imported; -// export const serializeEnvelope = serializeEnvelope_imported; -// export const createClientReportEnvelope = createClientReportEnvelope_imported; -// export const DEFAULT_RETRY_AFTER = DEFAULT_RETRY_AFTER_imported; -// export const disabledUntil = disabledUntil_imported; -// export const isRateLimited = isRateLimited_imported; -// export const parseRetryAfterHeader = parseRetryAfterHeader_imported; -// export const updateRateLimits = updateRateLimits_imported; -// export const eventFromMessage = eventFromMessage_imported; -// export const eventFromUnknownInput = eventFromUnknownInput_imported; -// export const exceptionFromError = exceptionFromError_imported; -// export const parseStackFrames = parseStackFrames_imported; -// export const callFrameToStackFrame = callFrameToStackFrame_imported; -// export const watchdogTimer = watchdogTimer_imported; -// export const LRUMap = LRUMap_imported; -// export const generatePropagationContext = generatePropagationContext_imported; -// export const vercelWaitUntil = vercelWaitUntil_imported; -// export const SDK_VERSION = SDK_VERSION_imported; -// export const getDebugImagesForResources = getDebugImagesForResources_imported; -// export const getFilenameToDebugIdMap = getFilenameToDebugIdMap_imported; -// export const escapeStringForRegex = escapeStringForRegex_imported; -// export const basename = basename_imported; -// export const dirname = dirname_imported; -// export const isAbsolute = isAbsolute_imported; -// export const join = join_imported; -// export const normalizePath = normalizePath_imported; -// export const relative = relative_imported; -// export const resolve = resolve_imported; -// export const getComponentName = getComponentName_imported; -// export const getDomElement = getDomElement_imported; -// export const getLocationHref = getLocationHref_imported; -// export const htmlTreeAsString = htmlTreeAsString_imported; -// export const isNativeFunction = isNativeFunction_imported; -// export const supportsDOMError = supportsDOMError_imported; -// export const supportsDOMException = supportsDOMException_imported; -// export const supportsErrorEvent = supportsErrorEvent_imported; -// export const supportsFetch = supportsFetch_imported; -// export const supportsNativeFetch = supportsNativeFetch_imported; -// export const supportsReferrerPolicy = supportsReferrerPolicy_imported; -// export const supportsReportingObserver = supportsReportingObserver_imported; -// export const _browserPerformanceTimeOriginMode = _browserPerformanceTimeOriginMode_imported; -// export const browserPerformanceTimeOrigin = browserPerformanceTimeOrigin_imported; -// export const supportsHistory = supportsHistory_imported; -// export const dynamicRequire = dynamicRequire_imported; -// export const isNodeEnv = isNodeEnv_imported; -// export const loadModule = loadModule_imported; -// export const flatten = flatten_imported; -// export const memoBuilder = memoBuilder_imported; -// export const arrayify = arrayify_imported; -// export const normalizeUrlToBase = normalizeUrlToBase_imported; -// export const urlEncode = urlEncode_imported; -// export const extractPathForTransaction = extractPathForTransaction_imported; -// export const DEFAULT_USER_INCLUDES = DEFAULT_USER_INCLUDES_imported; -// export const extractRequestData = extractRequestData_imported; -// export const addRequestDataToEvent = addRequestDataToEvent_imported; -// export const _asyncNullishCoalesce = _asyncNullishCoalesce_imported; -// export const _asyncOptionalChain = _asyncOptionalChain_imported; -// export const _asyncOptionalChainDelete = _asyncOptionalChainDelete_imported; -// export const _nullishCoalesce = _nullishCoalesce_imported; -// export const _optionalChain = _optionalChain_imported; -// export const _optionalChainDelete = _optionalChainDelete_imported; -// export const BAGGAGE_HEADER_NAME = BAGGAGE_HEADER_NAME_imported; -// export const getNumberOfUrlSegments = getNumberOfUrlSegments_imported; -// export const getSanitizedUrlString = getSanitizedUrlString_imported; -// export const parseUrl = parseUrl_imported; -// export const stripUrlQueryAndFragment = stripUrlQueryAndFragment_imported; -// export const makeFifoCache = makeFifoCache_imported; - -// import type { -// InternalGlobal as InternalGlobal_imported, -// PromiseBuffer as PromiseBuffer_imported, -// RateLimits as RateLimits_imported, -// AddRequestDataToEventOptions as AddRequestDataToEventOptions_imported, -// SdkSource as SdkSource_imported, -// TransactionNamingScheme as TransactionNamingScheme_imported, -// } from '@sentry/core'; - -// export type InternalGlobal = InternalGlobal_imported; -// export type SdkSource = SdkSource_imported; -// export type RateLimits = RateLimits_imported; -// export type AddRequestDataToEventOptions = AddRequestDataToEventOptions_imported; -// export type PromiseBuffer = PromiseBuffer_imported; -// export type TransactionNamingScheme = TransactionNamingScheme_imported; +/** @deprecated Import from `@sentry/core` instead. */ +export type InternalGlobal = InternalGlobal_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export type SdkSource = SdkSource_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export type RateLimits = RateLimits_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export type AddRequestDataToEventOptions = AddRequestDataToEventOptions_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export type PromiseBuffer = PromiseBuffer_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export type TransactionNamingScheme = TransactionNamingScheme_imported; diff --git a/packages/vue/src/integration.ts b/packages/vue/src/integration.ts index c7b7145ac4b1..22f394d72720 100644 --- a/packages/vue/src/integration.ts +++ b/packages/vue/src/integration.ts @@ -1,7 +1,4 @@ -import { defineIntegration, hasTracingEnabled } from '@sentry/core'; -import { GLOBAL_OBJ, arrayify, consoleSandbox } from '@sentry/core'; -import type { Client, IntegrationFn } from '@sentry/types'; - +import { GLOBAL_OBJ, consoleSandbox, defineIntegration, hasTracingEnabled } from '@sentry/core'; import { DEFAULT_HOOKS } from './constants'; import { DEBUG_BUILD } from './debug-build'; import { attachErrorHandler } from './errorhandler'; @@ -22,38 +19,30 @@ const DEFAULT_CONFIG: VueOptions = { const INTEGRATION_NAME = 'Vue'; -const _vueIntegration = ((integrationOptions: Partial = {}) => { +export const vueIntegration = defineIntegration((integrationOptions: Partial = {}) => { return { name: INTEGRATION_NAME, setup(client) { - _setupIntegration(client, integrationOptions); + const options: Options = { ...DEFAULT_CONFIG, ...client.getOptions(), ...integrationOptions }; + if (!options.Vue && !options.app) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. Update your `Sentry.init` call with an appropriate config option: `app` (Application Instance - Vue 3) or `Vue` (Vue Constructor - Vue 2).', + ); + }); + return; + } + + if (options.app) { + const apps = Array.isArray(options.app) ? options.app : [options.app]; + apps.forEach(app => vueInit(app, options)); + } else if (options.Vue) { + vueInit(options.Vue, options); + } }, }; -}) satisfies IntegrationFn; - -export const vueIntegration = defineIntegration(_vueIntegration); - -function _setupIntegration(client: Client, integrationOptions: Partial): void { - const options: Options = { ...DEFAULT_CONFIG, ...client.getOptions(), ...integrationOptions }; - if (!options.Vue && !options.app) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - `[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. -Update your \`Sentry.init\` call with an appropriate config option: -\`app\` (Application Instance - Vue 3) or \`Vue\` (Vue Constructor - Vue 2).`, - ); - }); - return; - } - - if (options.app) { - const apps = arrayify(options.app); - apps.forEach(app => vueInit(app, options)); - } else if (options.Vue) { - vueInit(options.Vue, options); - } -} +}); const vueInit = (app: Vue, options: Options): void => { if (DEBUG_BUILD) { @@ -85,6 +74,7 @@ const vueInit = (app: Vue, options: Options): void => { app.mixin( createTracingMixins({ ...options, + // eslint-disable-next-line deprecation/deprecation ...options.tracingOptions, }), ); diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index 9735923cd52c..8b23a2389e69 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -26,7 +26,7 @@ export type ViewModel = { }; }; -export interface VueOptions extends TracingOptions { +export interface VueOptions { /** Vue constructor to be used inside the integration (as imported by `import Vue from 'vue'` in Vue2) */ Vue?: Vue; @@ -60,9 +60,64 @@ export interface VueOptions extends TracingOptions { /** {@link TracingOptions} */ tracingOptions?: Partial; + + /** + * Decides whether to track components by hooking into its lifecycle methods. + * Can be either set to `boolean` to enable/disable tracking for all of them. + * Or to an array of specific component names (case-sensitive). + * + * @deprecated Use tracingOptions + */ + trackComponents: boolean | string[]; + + /** + * How long to wait until the tracked root activity is marked as finished and sent of to Sentry + * + * @deprecated Use tracingOptions + */ + timeout: number; + + /** + * List of hooks to keep track of during component lifecycle. + * Available hooks: 'activate' | 'create' | 'destroy' | 'mount' | 'unmount' | 'update' + * Based on https://vuejs.org/v2/api/#Options-Lifecycle-Hooks + * + * @deprecated Use tracingOptions + */ + hooks: Operation[]; } -export interface Options extends BrowserOptions, VueOptions {} +export interface Options extends BrowserOptions, VueOptions { + /** + * @deprecated Use `vueIntegration` tracingOptions + */ + tracingOptions?: Partial; + + /** + * Decides whether to track components by hooking into its lifecycle methods. + * Can be either set to `boolean` to enable/disable tracking for all of them. + * Or to an array of specific component names (case-sensitive). + * + * @deprecated Use `vueIntegration` tracingOptions + */ + trackComponents: boolean | string[]; + + /** + * How long to wait until the tracked root activity is marked as finished and sent of to Sentry + * + * @deprecated Use `vueIntegration` tracingOptions + */ + timeout: number; + + /** + * List of hooks to keep track of during component lifecycle. + * Available hooks: 'activate' | 'create' | 'destroy' | 'mount' | 'unmount' | 'update' + * Based on https://vuejs.org/v2/api/#Options-Lifecycle-Hooks + * + * @deprecated Use `vueIntegration` tracingOptions + */ + hooks: Operation[]; +} /** Vue specific configuration for Tracing Integration */ export interface TracingOptions { diff --git a/packages/vue/test/integration/init.test.ts b/packages/vue/test/integration/init.test.ts index fd9d7c56fc93..699f99a7057c 100644 --- a/packages/vue/test/integration/init.test.ts +++ b/packages/vue/test/integration/init.test.ts @@ -85,9 +85,7 @@ describe('Sentry.init', () => { app.mount(el); expect(warnings).toEqual([ - `[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. -Update your \`Sentry.init\` call with an appropriate config option: -\`app\` (Application Instance - Vue 3) or \`Vue\` (Vue Constructor - Vue 2).`, + '[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. Update your `Sentry.init` call with an appropriate config option: `app` (Application Instance - Vue 3) or `Vue` (Vue Constructor - Vue 2).', ]); });