diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c70bdbf89d0c..eb81eb50d581 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -8,4 +8,7 @@ 2aa4e94b036675245290596884959e06dcced044 # chore: Rename `integration-tests` -> `browser-integration-tests` (#7455) -ef6b3c7877d5fc8031c08bb28b0ffafaeb01f501 \ No newline at end of file +ef6b3c7877d5fc8031c08bb28b0ffafaeb01f501 + +# chore: Enforce formatting of MD files in repository root #10127 +aecf26f22dbf65ce2c0caadc4ce71b46266c9f45 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 413dd3d88aa3..83fb3f0be7b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,6 +35,7 @@ env: # packages/utils/cjs and packages/utils/esm: Symlinks to the folders inside of `build`, needed for tests CACHED_BUILD_PATHS: | + ${{ github.workspace }}/dev-packages/*/build ${{ github.workspace }}/packages/*/build ${{ github.workspace }}/packages/ember/*.d.ts ${{ github.workspace }}/packages/gatsby/*.d.ts @@ -858,6 +859,7 @@ jobs: matrix: test-application: [ + 'cloudflare-astro', 'node-express-app', 'create-react-app', 'create-next-app', @@ -944,6 +946,16 @@ jobs: timeout-minutes: 5 run: yarn test:assert + - name: Deploy Astro to Cloudflare + uses: cloudflare/pages-action@v1 + if: matrix.test-application == 'cloudflare-astro' + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} + directory: dist + workingDirectory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} + job_required_jobs_passed: name: All required jobs passed or were skipped needs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf94ab74c14..70359e5a52d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,70 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.93.0 + +### Important Changes + +#### Deprecations + +As we're moving closer to the next major version of the SDK, more public APIs were deprecated. + +To get a head start on migrating to the replacement APIs, please take a look at our +[migration guide](https://github.com/getsentry/sentry-javascript/blob/develop/MIGRATION.md). + +- feat(core): Deprecate `getActiveTransaction()` & `scope.getTransaction()` (#10098) +- feat(core): Deprecate `Hub.shouldSendDefaultPii` (#10062) +- feat(core): Deprecate `new Transaction()` (#10125) +- feat(core): Deprecate `scope.getSpan()` & `scope.setSpan()` (#10114) +- feat(core): Deprecate `scope.setTransactionName()` (#10113) +- feat(core): Deprecate `span.startChild()` (#10091) +- feat(core): Deprecate `startTransaction()` (#10073) +- feat(core): Deprecate `Transaction.getDynamicSamplingContext` in favor of `getDynamicSamplingContextFromSpan` (#10094) +- feat(core): Deprecate arguments for `startSpan()` (#10101) +- feat(core): Deprecate hub capture APIs and add them to `Scope` (#10039) +- feat(core): Deprecate session APIs on hub and add global replacements (#10054) +- feat(core): Deprecate span `name` and `description` (#10056) +- feat(core): Deprecate span `tags`, `data`, `context` & setters (#10053) +- feat(core): Deprecate transaction metadata in favor of attributes (#10097) +- feat(core): Deprecate `span.sampled` in favor of `span.isRecording()` (#10034) +- ref(node-experimental): Deprecate `lastEventId` on scope (#10093) + +#### Cron Monitoring Support for `node-schedule` library + +This release adds auto instrumented check-ins for the `node-schedule` library. + +```ts +import * as Sentry from '@sentry/node'; +import * as schedule from 'node-schedule'; + +const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule); + +const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * *', () => { + console.log('You will see this message every minute'); +}); +``` + +- feat(node): Instrumentation for `node-schedule` library (#10086) + +### Other Changes + +- feat(core): Add `span.spanContext()` (#10037) +- feat(core): Add `spanToJSON()` method to get span properties (#10074) +- feat(core): Allow to pass `scope` to `startSpan` APIs (#10076) +- feat(core): Allow to pass start/end timestamp for spans flexibly (#10060) +- feat(node): Make `getModuleFromFilename` compatible with ESM (#10061) +- feat(replay): Update rrweb to 2.7.3 (#10072) +- feat(utils): Add `parameterize` function (#9145) +- fix(astro): Use correct package name for CF (#10099) +- fix(core): Do not run `setup` for integration on client multiple times (#10116) +- fix(core): Ensure we copy passed in span data/tags/attributes (#10105) +- fix(cron): Make name required for instrumentNodeCron option (#10070) +- fix(nextjs): Don't capture not-found and redirect errors in generation functions (#10057) +- fix(node): `LocalVariables` integration should have correct name (#10084) +- fix(node): Anr events should have an `event_id` (#10068) +- fix(node): Revert to only use sync debugger for `LocalVariables` (#10077) +- fix(node): Update ANR min node version to v16.17.0 (#10107) + ## 7.92.0 ### Important Changes @@ -18,14 +82,16 @@ - ref: Deprecate `deepReadDirSync` (#10016) - ref: Deprecate `lastEventId()` (#10043) -Please take a look at the [Migration docs](./MIGRATION.md) for more details. These methods will be removed in the upcoming [v8 major release](https://github.com/getsentry/sentry-javascript/discussions/9802). +Please take a look at the [Migration docs](./MIGRATION.md) for more details. These methods will be removed in the +upcoming [v8 major release](https://github.com/getsentry/sentry-javascript/discussions/9802). #### Cron Monitoring Support for `cron` and `node-cron` libraries - feat(node): Instrumentation for `cron` library (#9999) - feat(node): Instrumentation for `node-cron` library (#9904) -This release adds instrumentation for the `cron` and `node-cron` libraries. This allows you to monitor your cron jobs with [Sentry cron monitors](https://docs.sentry.io/product/crons/). +This release adds instrumentation for the `cron` and `node-cron` libraries. This allows you to monitor your cron jobs +with [Sentry cron monitors](https://docs.sentry.io/product/crons/). For [`cron`](https://www.npmjs.com/package/cron): @@ -78,7 +144,7 @@ cronWithCheckIn.schedule( - fix(astro): Handle non-utf8 encoded streams in middleware (#9989) - fix(astro): prevent sentry from externalized (#9994) - fix(core): Ensure `withScope` sets current scope correctly with async callbacks (#9974) -- fix(node): ANR fixes and additions (#9998) +- fix(node): ANR fixes and additions (#9998) - fix(node): Anr should not block exit (#10035) - fix(node): Correctly resolve module name (#10001) - fix(node): Handle inspector already open (#10025) @@ -97,7 +163,9 @@ Work in this release contributed by @joshkel. Thank you for your contribution! - **feat: Add server runtime metrics aggregator (#9894)** -The release adds alpha support for [Sentry developer metrics](https://github.com/getsentry/sentry/discussions/58584) in the server runtime SDKs (`@sentry/node`, `@sentry/deno`, `@sentry/nextjs` server-side, etc.). Via the newly introduced APIs, you can now flush metrics directly to Sentry. +The release adds alpha support for [Sentry developer metrics](https://github.com/getsentry/sentry/discussions/58584) in +the server runtime SDKs (`@sentry/node`, `@sentry/deno`, `@sentry/nextjs` server-side, etc.). Via the newly introduced +APIs, you can now flush metrics directly to Sentry. To enable capturing metrics, you first need to add the `metricsAggregator` experiment to your `Sentry.init` call. @@ -128,7 +196,10 @@ Sentry.metrics.set('valuable.ids', 2); - **feat(node): Rework ANR to use worker script via an integration (#9945)** -The [ANR tracking integration for Node](https://docs.sentry.io/platforms/node/configuration/application-not-responding/) has been reworked to use an integration. ANR tracking now requires a minimum Node version of 16 or higher. Previously you had to call `Sentry.enableANRDetection` before running your application, now you can simply add the `Anr` integration to your `Sentry.init` call. +The [ANR tracking integration for Node](https://docs.sentry.io/platforms/node/configuration/application-not-responding/) +has been reworked to use an integration. ANR tracking now requires a minimum Node version of 16 or higher. Previously +you had to call `Sentry.enableANRDetection` before running your application, now you can simply add the `Anr` +integration to your `Sentry.init` call. ```js import * as Sentry from '@sentry/node'; @@ -168,7 +239,8 @@ Sentry.init({ - **feat(core): Deprecate `configureScope` (#9887)** - **feat(core): Deprecate `pushScope` & `popScope` (#9890)** -This release deprecates `configureScope`, `pushScope`, and `popScope`, which will be removed in the upcoming v8 major release. +This release deprecates `configureScope`, `pushScope`, and `popScope`, which will be removed in the upcoming v8 major +release. #### Hapi Integration @@ -181,23 +253,21 @@ const Sentry = require('@sentry/node'); const Hapi = require('@hapi/hapi'); const init = async () => { - const server = Hapi.server({ - // your server configuration ... - }); + const server = Hapi.server({ + // your server configuration ... + }); - Sentry.init({ - dsn: '__DSN__', - tracesSampleRate: 1.0, - integrations: [ - new Sentry.Integrations.Hapi({ server }), - ], - }); + Sentry.init({ + dsn: '__DSN__', + tracesSampleRate: 1.0, + integrations: [new Sentry.Integrations.Hapi({ server })], + }); - server.route({ - // your route configuration ... - }); + server.route({ + // your route configuration ... + }); - await server.start(); + await server.start(); }; ``` @@ -205,7 +275,9 @@ const init = async () => { - **chore(sveltekit): Add SvelteKit 2.0 to peer dependencies (#9861)** -This release adds support for SvelteKit 2.0 in the `@sentry/sveltekit` package. If you're upgrading from SvelteKit 1.x to 2.x and already use the Sentry SvelteKit SDK, no changes apart from upgrading to this (or a newer) version are necessary. +This release adds support for SvelteKit 2.0 in the `@sentry/sveltekit` package. If you're upgrading from SvelteKit 1.x +to 2.x and already use the Sentry SvelteKit SDK, no changes apart from upgrading to this (or a newer) version are +necessary. ### Other Changes @@ -233,16 +305,16 @@ Work in this release contributed by @adam187, and @jghinestrosa. Thank you for y - **feat(browser): Add browser metrics sdk (#9794)** -The release adds alpha support for [Sentry developer metrics](https://github.com/getsentry/sentry/discussions/58584) in the Browser SDKs (`@sentry/browser` and related framework SDKs). Via the newly introduced APIs, you can now flush metrics directly to Sentry. +The release adds alpha support for [Sentry developer metrics](https://github.com/getsentry/sentry/discussions/58584) in +the Browser SDKs (`@sentry/browser` and related framework SDKs). Via the newly introduced APIs, you can now flush +metrics directly to Sentry. To enable capturing metrics, you first need to add the `MetricsAggregator` integration. ```js Sentry.init({ dsn: '__DSN__', - integrations: [ - new Sentry.metrics.MetricsAggregator(), - ], + integrations: [new Sentry.metrics.MetricsAggregator()], }); ``` @@ -266,17 +338,17 @@ In a future release we'll add support for server runtimes (Node, Deno, Bun, Verc - **feat(deno): Optionally instrument `Deno.cron` (#9808)** -This releases add support for instrumenting [Deno cron's](https://deno.com/blog/cron) with [Sentry cron monitors](https://docs.sentry.io/product/crons/). This requires v1.38 of Deno run with the `--unstable` flag and the usage of the `DenoCron` Sentry integration. +This releases add support for instrumenting [Deno cron's](https://deno.com/blog/cron) with +[Sentry cron monitors](https://docs.sentry.io/product/crons/). This requires v1.38 of Deno run with the `--unstable` +flag and the usage of the `DenoCron` Sentry integration. ```ts // Import from the Deno registry -import * as Sentry from "https://deno.land/x/sentry/index.mjs"; +import * as Sentry from 'https://deno.land/x/sentry/index.mjs'; Sentry.init({ dsn: '__DSN__', - integrations: [ - new Sentry.DenoCron(), - ], + integrations: [new Sentry.DenoCron()], }); ``` @@ -325,10 +397,12 @@ Sentry.init({ - **ref(nextjs): Set `automaticVercelMonitors` to be `false` by default (#9697)** From this version onwards the default for the `automaticVercelMonitors` option in the Next.js SDK is set to false. -Previously, if you made use of Vercel Crons the SDK automatically instrumented the relevant routes to create Sentry monitors. -Because this feature will soon be generally available, we are now flipping the default to avoid situations where quota is used unexpectedly. +Previously, if you made use of Vercel Crons the SDK automatically instrumented the relevant routes to create Sentry +monitors. Because this feature will soon be generally available, we are now flipping the default to avoid situations +where quota is used unexpectedly. -If you want to continue using this feature, make sure to set the `automaticVercelMonitors` flag to `true` in your `next.config.js` Sentry settings. +If you want to continue using this feature, make sure to set the `automaticVercelMonitors` flag to `true` in your +`next.config.js` Sentry settings. ### Other Changes @@ -381,7 +455,8 @@ Work in this release contributed by @arya-s. Thank you for your contribution! - fix(node): Improve error handling and shutdown handling for ANR (#9548) - fix(tracing-internal): Fix case when originalURL contain query params (#9531) -Work in this release contributed by @powerfulyang, @LubomirIgonda1, @joshkel, and @alexgleason. Thank you for your contributions! +Work in this release contributed by @powerfulyang, @LubomirIgonda1, @joshkel, and @alexgleason. Thank you for your +contributions! ## 7.81.0 @@ -389,19 +464,21 @@ Work in this release contributed by @powerfulyang, @LubomirIgonda1, @joshkel, an **- feat(nextjs): Add instrumentation utility for server actions (#9553)** -This release adds a utility function `withServerActionInstrumentation` to the `@sentry/nextjs` SDK for instrumenting your Next.js server actions with error and performance monitoring. +This release adds a utility function `withServerActionInstrumentation` to the `@sentry/nextjs` SDK for instrumenting +your Next.js server actions with error and performance monitoring. -You can optionally pass form data and headers to record them, and configure the wrapper to record the Server Action responses: +You can optionally pass form data and headers to record them, and configure the wrapper to record the Server Action +responses: ```tsx -import * as Sentry from "@sentry/nextjs"; -import { headers } from "next/headers"; +import * as Sentry from '@sentry/nextjs'; +import { headers } from 'next/headers'; export default function ServerComponent() { async function myServerAction(formData: FormData) { - "use server"; + 'use server'; return await Sentry.withServerActionInstrumentation( - "myServerAction", // The name you want to associate this Server Action with in Sentry + 'myServerAction', // The name you want to associate this Server Action with in Sentry { formData, // Optionally pass in the form data headers: headers(), // Optionally pass in headers @@ -410,8 +487,8 @@ export default function ServerComponent() { async () => { // ... Your Server Action code - return { name: "John Doe" }; - } + return { name: 'John Doe' }; + }, ); } @@ -465,13 +542,14 @@ Work in this release contributed by @snoozbuster. Thank you for your contributio - **Replay Bundle Size improvements** -We've dramatically decreased the bundle size of our Replay package, reducing the minified & gzipped bundle size by ~20 KB! -This was possible by extensive use of tree shaking and a host of small changes to reduce our footprint: +We've dramatically decreased the bundle size of our Replay package, reducing the minified & gzipped bundle size by ~20 +KB! This was possible by extensive use of tree shaking and a host of small changes to reduce our footprint: - feat(replay): Update rrweb to 2.2.0 (#9414) - ref(replay): Use fflate instead of pako for compression (#9436) -By using [tree shaking](https://docs.sentry.io/platforms/javascript/configuration/tree-shaking/) it is possible to shave up to 10 additional KB off the bundle. +By using [tree shaking](https://docs.sentry.io/platforms/javascript/configuration/tree-shaking/) it is possible to shave +up to 10 additional KB off the bundle. ### Other Changes @@ -513,15 +591,16 @@ By using [tree shaking](https://docs.sentry.io/platforms/javascript/configuratio - **feat(core): Add cron monitor wrapper helper (#9395)** -This release adds `Sentry.withMonitor()`, a wrapping function that wraps a callback with a cron monitor that will automatically report completions and failures: +This release adds `Sentry.withMonitor()`, a wrapping function that wraps a callback with a cron monitor that will +automatically report completions and failures: ```ts -import * as Sentry from "@sentry/node"; +import * as Sentry from '@sentry/node'; // withMonitor() will send checkin when callback is started/finished // works with async and sync callbacks. const result = Sentry.withMonitor( - "dailyEmail", + 'dailyEmail', () => { // withCheckIn return value is same return value here return sendEmail(); @@ -529,12 +608,12 @@ const result = Sentry.withMonitor( // Optional upsert options { schedule: { - type: "crontab", - value: "0 * * * *", + type: 'crontab', + value: '0 * * * *', }, // 🇨🇦🫡 - timezone: "Canada/Eastern", - } + timezone: 'Canada/Eastern', + }, ); ``` @@ -565,9 +644,12 @@ Work in this release contributed by @LubomirIgonda1. Thank you for your contribu - **feat(opentelemetry): Add new `@sentry/opentelemetry` package (#9238)** -This release publishes a new package, `@sentry/opentelemetry`. This is a runtime agnostic replacement for `@sentry/opentelemetry-node` and exports a couple of useful utilities which can be used to use Sentry together with OpenTelemetry. +This release publishes a new package, `@sentry/opentelemetry`. This is a runtime agnostic replacement for +`@sentry/opentelemetry-node` and exports a couple of useful utilities which can be used to use Sentry together with +OpenTelemetry. -You can read more about [@sentry/opentelemetry in the Readme](https://github.com/getsentry/sentry-javascript/tree/develop/packages/opentelemetry). +You can read more about +[@sentry/opentelemetry in the Readme](https://github.com/getsentry/sentry-javascript/tree/develop/packages/opentelemetry). - **feat(replay): Allow to treeshake rrweb features (#9274)** @@ -577,7 +659,8 @@ Starting with this release, you can configure the following build-time flags in - `__RRWEB_EXCLUDE_IFRAME__` - `__RRWEB_EXCLUDE_SHADOW_DOM__` -You can read more about [tree shaking in our docs](https://docs.sentry.io/platforms/javascript/configuration/tree-shaking/). +You can read more about +[tree shaking in our docs](https://docs.sentry.io/platforms/javascript/configuration/tree-shaking/). ### Other Changes @@ -620,22 +703,22 @@ Work in this release contributed by @LubomirIgonda1. Thank you for your contribu - **feat(astro): Add `sentryAstro` integration (#9218)** -This Release introduces the first alpha version of our new SDK for Astro. -At this time, the SDK is considered experimental and things might break and change in future versions. +This Release introduces the first alpha version of our new SDK for Astro. At this time, the SDK is considered +experimental and things might break and change in future versions. The core of the SDK is an Astro integration which you easily add to your Astro config: ```js // astro.config.js -import { defineConfig } from "astro/config"; -import sentry from "@sentry/astro"; +import { defineConfig } from 'astro/config'; +import sentry from '@sentry/astro'; export default defineConfig({ integrations: [ sentry({ - dsn: "__DSN__", + dsn: '__DSN__', sourceMapsUploadOptions: { - project: "astro", + project: 'astro', authToken: process.env.SENTRY_AUTH_TOKEN, }, }), @@ -681,9 +764,14 @@ Work in this release contributed by @aldenquimby. Thank you for your contributio - **feat(replay): Upgrade to rrweb2** -This is fully backwards compatible with prior versions of the Replay SDK. The only breaking change that we will making is to not be masking `aria-label` by default. The reason for this change is to align with our core SDK which also does not mask `aria-label`. This change also enables better support of searching by clicks. +This is fully backwards compatible with prior versions of the Replay SDK. The only breaking change that we will making +is to not be masking `aria-label` by default. The reason for this change is to align with our core SDK which also does +not mask `aria-label`. This change also enables better support of searching by clicks. -Another change that needs to be highlighted is the 13% bundle size increase. This bundle size increase is necessary to bring improved recording performance and improved replay fidelity, especially in regards to web components and iframes. We will be investigating the reduction of the bundle size in [this PR](https://github.com/getsentry/sentry-javascript/issues/8815). +Another change that needs to be highlighted is the 13% bundle size increase. This bundle size increase is necessary to +bring improved recording performance and improved replay fidelity, especially in regards to web components and iframes. +We will be investigating the reduction of the bundle size in +[this PR](https://github.com/getsentry/sentry-javascript/issues/8815). Here are benchmarks comparing the version 1 of rrweb to version 2 @@ -719,20 +807,21 @@ Work in this release contributed by @vlad-zhukov. Thank you for your contributio - **feat(node): App Not Responding with stack traces (#9079)** -This release introduces support for Application Not Responding (ANR) errors for Node.js applications. -These errors are triggered when the Node.js main thread event loop of an application is blocked for more than five seconds. -The Node SDK reports ANR errors as Sentry events and can optionally attach a stacktrace of the blocking code to the ANR event. +This release introduces support for Application Not Responding (ANR) errors for Node.js applications. These errors are +triggered when the Node.js main thread event loop of an application is blocked for more than five seconds. The Node SDK +reports ANR errors as Sentry events and can optionally attach a stacktrace of the blocking code to the ANR event. -To enable ANR detection, import and use the `enableANRDetection` function from the `@sentry/node` package before you run the rest of your application code. -Any event loop blocking before calling `enableANRDetection` will not be detected by the SDK. +To enable ANR detection, import and use the `enableANRDetection` function from the `@sentry/node` package before you run +the rest of your application code. Any event loop blocking before calling `enableANRDetection` will not be detected by +the SDK. Example (ESM): ```ts -import * as Sentry from "@sentry/node"; +import * as Sentry from '@sentry/node'; Sentry.init({ - dsn: "___PUBLIC_DSN___", + dsn: '___PUBLIC_DSN___', tracesSampleRate: 1.0, }); @@ -744,10 +833,10 @@ runApp(); Example (CJS): ```ts -const Sentry = require("@sentry/node"); +const Sentry = require('@sentry/node'); Sentry.init({ - dsn: "___PUBLIC_DSN___", + dsn: '___PUBLIC_DSN___', tracesSampleRate: 1.0, }); @@ -780,13 +869,17 @@ Work in this release contributed by @jorrit. Thank you for your contribution! - **feat: Add Bun SDK (#9029)** -This release contains the beta version of `@sentry/bun`, our SDK for the [Bun JavaScript runtime](https://bun.sh/)! For details on how to use it, please see the [README](./packages/bun/README.md). Any feedback/bug reports are greatly appreciated, please [reach out on GitHub](https://github.com/getsentry/sentry-javascript/discussions/7979). +This release contains the beta version of `@sentry/bun`, our SDK for the [Bun JavaScript runtime](https://bun.sh/)! For +details on how to use it, please see the [README](./packages/bun/README.md). Any feedback/bug reports are greatly +appreciated, please [reach out on GitHub](https://github.com/getsentry/sentry-javascript/discussions/7979). -Note that as of now the Bun runtime does not support global error handlers. This is being actively worked on, see [the tracking issue in Bun's GitHub repo](https://github.com/oven-sh/bun/issues/5091). +Note that as of now the Bun runtime does not support global error handlers. This is being actively worked on, see +[the tracking issue in Bun's GitHub repo](https://github.com/oven-sh/bun/issues/5091). - **feat(remix): Add Remix 2.x release support. (#8940)** -The Sentry Remix SDK now officially supports Remix v2! See [our Remix docs for more details](https://docs.sentry.io/platforms/javascript/guides/remix/). +The Sentry Remix SDK now officially supports Remix v2! See +[our Remix docs for more details](https://docs.sentry.io/platforms/javascript/guides/remix/). ### Other Changes @@ -802,7 +895,8 @@ The Sentry Remix SDK now officially supports Remix v2! See [our Remix docs for m Work in this release contributed by @Dima-Dim, @krist7599555 and @lifeiscontent. Thank you for your contributions! -Special thanks for @isaacharrisholt for helping us implement a Vercel Edge Runtime SDK which we use under the hood for our Next.js SDK. +Special thanks for @isaacharrisholt for helping us implement a Vercel Edge Runtime SDK which we use under the hood for +our Next.js SDK. ## 7.69.0 @@ -812,32 +906,37 @@ Special thanks for @isaacharrisholt for helping us implement a Vercel Edge Runti - feat: Update span performance API names (#8971) - feat(core): Introduce startSpanManual (#8913) -This release introduces a new set of top level APIs for the Performance Monitoring SDKs. These aim to simplify creating spans and reduce the boilerplate needed for performance instrumentation. The three new methods introduced are `Sentry.startSpan`, `Sentry.startInactiveSpan`, and `Sentry.startSpanManual`. These methods are available in the browser and node SDKs. +This release introduces a new set of top level APIs for the Performance Monitoring SDKs. These aim to simplify creating +spans and reduce the boilerplate needed for performance instrumentation. The three new methods introduced are +`Sentry.startSpan`, `Sentry.startInactiveSpan`, and `Sentry.startSpanManual`. These methods are available in the browser +and node SDKs. -`Sentry.startSpan` wraps a callback in a span. The span is automatically finished when the callback returns. This is the recommended way to create spans. +`Sentry.startSpan` wraps a callback in a span. The span is automatically finished when the callback returns. This is the +recommended way to create spans. ```js // Start a span that tracks the duration of expensiveFunction -const result = Sentry.startSpan({ name: "important function" }, () => { +const result = Sentry.startSpan({ name: 'important function' }, () => { return expensiveFunction(); }); // You can also mutate the span wrapping the callback to set data or status -Sentry.startSpan({ name: "important function" }, (span) => { +Sentry.startSpan({ name: 'important function' }, span => { // span is undefined if performance monitoring is turned off or if // the span was not sampled. This is done to reduce overhead. - span?.setData("version", "1.0.0"); + span?.setData('version', '1.0.0'); return expensiveFunction(); }); ``` -If you don't want the span to finish when the callback returns, use `Sentry.startSpanManual` to control when the span is finished. This is useful for event emitters or similar. +If you don't want the span to finish when the callback returns, use `Sentry.startSpanManual` to control when the span is +finished. This is useful for event emitters or similar. ```js // Start a span that tracks the duration of middleware function middleware(_req, res, next) { - return Sentry.startSpanManual({ name: "middleware" }, (span, finish) => { - res.once("finish", () => { + return Sentry.startSpanManual({ name: 'middleware' }, (span, finish) => { + res.once('finish', () => { span?.setHttpStatus(res.status); finish(); }); @@ -846,18 +945,21 @@ function middleware(_req, res, next) { } ``` -`Sentry.startSpan` and `Sentry.startSpanManual` create a span and make it active for the duration of the callback. Any spans created while this active span is running will be added as a child span to it. If you want to create a span without making it active, use `Sentry.startInactiveSpan`. This is useful for creating parallel spans that are not related to each other. +`Sentry.startSpan` and `Sentry.startSpanManual` create a span and make it active for the duration of the callback. Any +spans created while this active span is running will be added as a child span to it. If you want to create a span +without making it active, use `Sentry.startInactiveSpan`. This is useful for creating parallel spans that are not +related to each other. ```js -const span1 = Sentry.startInactiveSpan({ name: "span1" }); +const span1 = Sentry.startInactiveSpan({ name: 'span1' }); someWork(); -const span2 = Sentry.startInactiveSpan({ name: "span2" }); +const span2 = Sentry.startInactiveSpan({ name: 'span2' }); moreWork(); -const span3 = Sentry.startInactiveSpan({ name: "span3" }); +const span3 = Sentry.startInactiveSpan({ name: 'span3' }); evenMoreWork(); @@ -914,13 +1016,13 @@ Work in this release contributed by @Duncanxyz and @malay44. Thank you for your - feat(serverless): Mark errors caught in Serverless handlers as unhandled (#8907) - feat(vue): Mark errors caught by Vue wrappers as unhandled (#8905) -This release fixes inconsistent behaviour of when our SDKs classify captured errors as unhandled. -Previously, some of our instrumentations correctly set unhandled, while others set handled. -Going forward, all errors caught automatically from our SDKs will be marked as unhandled. -If you manually capture errors (e.g. by calling `Sentry.captureException`), your errors will continue to be reported as handled. +This release fixes inconsistent behaviour of when our SDKs classify captured errors as unhandled. Previously, some of +our instrumentations correctly set unhandled, while others set handled. Going forward, all errors caught automatically +from our SDKs will be marked as unhandled. If you manually capture errors (e.g. by calling `Sentry.captureException`), +your errors will continue to be reported as handled. -This change might lead to a decrease in reported crash-free sessions and consequently in your release health score. -If you have concerns about this, feel free to open an issue. +This change might lead to a decrease in reported crash-free sessions and consequently in your release health score. If +you have concerns about this, feel free to open an issue. ### Other Changes @@ -978,7 +1080,8 @@ Work in this release contributed by @SorsOps. Thank you for your contribution! - feat(tracing): Add db connection attributes for mysql spans (#8775) - feat(tracing): Add db connection attributes for postgres spans (#8778) - feat(tracing): Improve data collection for mongodb spans (#8774) -- fix(nextjs): Execute sentry config independently of `autoInstrumentServerFunctions` and `autoInstrumentAppDirectory` (#8781) +- fix(nextjs): Execute sentry config independently of `autoInstrumentServerFunctions` and `autoInstrumentAppDirectory` + (#8781) - fix(replay): Ensure we do not flush if flush took too long (#8784) - fix(replay): Ensure we do not try to flush when we force stop replay (#8783) - fix(replay): Fix `hasCheckout` handling (#8782) @@ -991,15 +1094,18 @@ Work in this release contributed by @SorsOps. Thank you for your contribution! - **feat(integrations): Add `ContextLines` integration for html-embedded JS stack frames (#8699)** -This release adds the `ContextLines` integration as an optional integration for the Browser SDKs to `@sentry/integrations`. +This release adds the `ContextLines` integration as an optional integration for the Browser SDKs to +`@sentry/integrations`. -This integration adds source code from inline JavaScript of the current page's HTML (e.g. JS in ` + diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts new file mode 100644 index 000000000000..372ca8978356 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts @@ -0,0 +1,71 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers'; + +sentryTest('can record canvas', async ({ getLocalTestUrl, page, browserName }) => { + if (shouldSkipReplayTest() || browserName === 'webkit') { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const reqPromise2 = waitForReplayRequest(page, 2); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + await reqPromise0; + await Promise.all([page.click('#draw'), reqPromise1]); + + const { incrementalSnapshots } = getReplayRecordingContent(await reqPromise2); + expect(incrementalSnapshots).toEqual( + expect.arrayContaining([ + { + data: { + commands: [ + { + args: [0, 0, 150, 150], + property: 'clearRect', + }, + { + args: [ + { + args: [ + { + data: [ + { + base64: expect.any(String), + rr_type: 'ArrayBuffer', + }, + ], + rr_type: 'Blob', + type: 'image/webp', + }, + ], + rr_type: 'ImageBitmap', + }, + 0, + 0, + ], + property: 'drawImage', + }, + ], + id: 9, + source: 9, + type: 0, + }, + timestamp: 0, + type: 3, + }, + ]), + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/template.html b/dev-packages/browser-integration-tests/suites/replay/canvas/template.html new file mode 100644 index 000000000000..2b3e2f0b27b4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts index 4468a254bde4..63c103e51259 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts @@ -33,7 +33,10 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; @@ -78,7 +81,10 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; @@ -135,7 +141,10 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; @@ -183,7 +192,10 @@ sentryTest( await page.evaluate(async () => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; diff --git a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js new file mode 100644 index 000000000000..c22f576ca681 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '0.1', + initialScope: { + user: { + id: '1337', + email: 'user@name.com', + username: 'user1337', + }, + }, + debug: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/template.html b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/template.html new file mode 100644 index 000000000000..77906444cbce --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/template.html @@ -0,0 +1,9 @@ + + + + + + + Navigate + + diff --git a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts new file mode 100644 index 000000000000..b7de815b7825 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts @@ -0,0 +1,41 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { SessionContext } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest('should start a new session on pageload.', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + const session = await getFirstSentryEnvelopeRequest(page, url); + + expect(session).toBeDefined(); + expect(session.init).toBe(true); + expect(session.errors).toBe(0); + expect(session.status).toBe('ok'); + expect(session.did).toBe('1337'); +}); + +sentryTest('should start a new session with navigation.', async ({ getLocalTestPath, page, browserName }) => { + // Navigations get CORS error on Firefox and WebKit as we're using `file://` protocol. + if (browserName !== 'chromium') { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.route('**/foo', (route: Route) => route.fulfill({ path: `${__dirname}/dist/index.html` })); + + const initSession = await getFirstSentryEnvelopeRequest(page, url); + + await page.click('#navigate'); + + const newSession = await getFirstSentryEnvelopeRequest(page, url); + + expect(newSession).toBeDefined(); + expect(newSession.init).toBe(true); + expect(newSession.errors).toBe(0); + expect(newSession.status).toBe('ok'); + expect(newSession.sid).toBeDefined(); + expect(initSession.sid).not.toBe(newSession.sid); + expect(newSession.did).toBe('1337'); +}); diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js new file mode 100644 index 000000000000..4958e35f2198 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '0.1', + // intentionally disabling this, we want to leverage the deprecated hub API + autoSessionTracking: false, +}); + +// simulate old startSessionTracking behavior +Sentry.getCurrentHub().startSession({ ignoreDuration: true }); +Sentry.getCurrentHub().captureSession(); diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html new file mode 100644 index 000000000000..77906444cbce --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html @@ -0,0 +1,9 @@ + + + + + + + Navigate + + diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts new file mode 100644 index 000000000000..8a48f161c93b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts @@ -0,0 +1,39 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { SessionContext } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest('should start a new session on pageload.', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + const session = await getFirstSentryEnvelopeRequest(page, url); + + expect(session).toBeDefined(); + expect(session.init).toBe(true); + expect(session.errors).toBe(0); + expect(session.status).toBe('ok'); +}); + +sentryTest('should start a new session with navigation.', async ({ getLocalTestPath, page, browserName }) => { + // Navigations get CORS error on Firefox and WebKit as we're using `file://` protocol. + if (browserName !== 'chromium') { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.route('**/foo', (route: Route) => route.fulfill({ path: `${__dirname}/dist/index.html` })); + + const initSession = await getFirstSentryEnvelopeRequest(page, url); + + await page.click('#navigate'); + + const newSession = await getFirstSentryEnvelopeRequest(page, url); + + expect(newSession).toBeDefined(); + expect(newSession.init).toBe(true); + expect(newSession.errors).toBe(0); + expect(newSession.status).toBe('ok'); + expect(newSession.sid).toBeDefined(); + expect(initSession.sid).not.toBe(newSession.sid); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js b/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js index 7d000c0ac2cd..c2fcbb33a24c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js @@ -13,5 +13,8 @@ Sentry.init({ const scope = Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); -scope.setTransactionName('testTransactionDSC'); +scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; +}); scope.getTransaction().setMetadata({ source: 'custom' }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js b/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js index f382a49c153d..1528306fcbde 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js @@ -13,4 +13,7 @@ Sentry.init({ const scope = Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); -scope.setTransactionName('testTransactionDSC'); +scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts index 55e0b4d0e833..aaab7059320c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts @@ -21,6 +21,7 @@ sentryTest('should capture a FID vital.', async ({ browserName, getLocalTestPath expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.fid?.value).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation const fidSpan = eventData.spans?.filter(({ description }) => description === 'first input delay')[0]; expect(fidSpan).toBeDefined(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts index 3a97c62d7f68..4914c0b45779 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts @@ -16,6 +16,7 @@ sentryTest('should capture FP vital.', async ({ browserName, getLocalTestPath, p expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.fp?.value).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation const fpSpan = eventData.spans?.filter(({ description }) => description === 'first-paint')[0]; expect(fpSpan).toBeDefined(); @@ -34,6 +35,7 @@ sentryTest('should capture FCP vital.', async ({ getLocalTestPath, page }) => { expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.fcp?.value).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation const fcpSpan = eventData.spans?.filter(({ description }) => description === 'first-contentful-paint')[0]; expect(fcpSpan).toBeDefined(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/init.js new file mode 100644 index 000000000000..505fab06b330 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; +import { Integrations } from '@sentry/tracing'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Integrations.BrowserTracing({ tracingOrigins: ['http://example.com'] })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/subject.js new file mode 100644 index 000000000000..70a1992e9f8b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/subject.js @@ -0,0 +1,7 @@ +const request = new Request('http://example.com/api/test/', { + headers: { foo: '11' }, +}); + +fetch(request, { + headers: { bar: '22' }, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/test.ts new file mode 100644 index 000000000000..eb2f354dba4a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/test.ts @@ -0,0 +1,30 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'instrumentation should pass on headers from fetch options instead of init request, if set', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('**/api/test/', async route => { + const req = route.request(); + const headers = await req.allHeaders(); + + // headers.bar was set in fetch options (and should be sent) + expect(headers.bar).toBe('22'); + // headers.foo was set in init request object (and should be ignored) + expect(headers.foo).toBeUndefined(); + + return route.fulfill({ + status: 200, + body: 'ok', + }); + }); + + await getLocalTestPath({ testDir: __dirname }); + }, +); diff --git a/dev-packages/browser-integration-tests/utils/replayHelpers.ts b/dev-packages/browser-integration-tests/utils/replayHelpers.ts index 613bd5b447f1..2a951e4a215e 100644 --- a/dev-packages/browser-integration-tests/utils/replayHelpers.ts +++ b/dev-packages/browser-integration-tests/utils/replayHelpers.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import type { fullSnapshotEvent, incrementalSnapshotEvent } from '@sentry-internal/rrweb'; import { EventType } from '@sentry-internal/rrweb'; import type { ReplayEventWithTime } from '@sentry/browser'; @@ -5,6 +6,7 @@ import type { InternalEventContext, RecordingEvent, ReplayContainer, + ReplayPluginOptions, Session, } from '@sentry/replay/build/npm/types/types'; import type { Breadcrumb, Event, ReplayEvent, ReplayRecordingMode } from '@sentry/types'; @@ -171,6 +173,8 @@ export function getReplaySnapshot(page: Page): Promise<{ _isPaused: boolean; _isEnabled: boolean; _context: InternalEventContext; + _options: ReplayPluginOptions; + _hasCanvas: boolean; session: Session | undefined; recordingMode: ReplayRecordingMode; }> { @@ -182,6 +186,9 @@ export function getReplaySnapshot(page: Page): Promise<{ _isPaused: replay.isPaused(), _isEnabled: replay.isEnabled(), _context: replay.getContext(), + _options: replay.getOptions(), + // We cannot pass the function through as this is serialized + _hasCanvas: typeof replay.getOptions()._experiments.canvas?.manager === 'function', session: replay.session, recordingMode: replay.recordingMode, }; diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 2d708daa39c0..0c2484974bc0 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "7.92.0", + "version": "7.93.0", "license": "MIT", "private": true, "scripts": { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/.gitignore b/dev-packages/e2e-tests/test-applications/cloudflare-astro/.gitignore new file mode 100644 index 000000000000..6d4c0aa066aa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/.gitignore @@ -0,0 +1,21 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/astro.config.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-astro/astro.config.mjs new file mode 100644 index 000000000000..4e2493250a8f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/astro.config.mjs @@ -0,0 +1,24 @@ +import cloudflare from '@astrojs/cloudflare'; +import sentry from '@sentry/astro'; +import { defineConfig } from 'astro/config'; + +const dsn = process.env.E2E_TEST_DSN; + +// https://astro.build/config +export default defineConfig({ + output: 'hybrid', + adapter: cloudflare({ + imageService: 'passthrough', + }), + integrations: [ + sentry({ + enabled: Boolean(dsn), + dsn, + sourceMapsUploadOptions: { + enabled: false, + }, + clientInitPath: 'sentry.client.mjs', + serverInitPath: 'sentry.server.mjs', + }), + ], +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json new file mode 100644 index 000000000000..5ac3adc9da61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json @@ -0,0 +1,27 @@ +{ + "name": "cloudflare-astro", + "type": "module", + "version": "0.0.1", + "private": true, + "scripts": { + "astro": "astro", + "build": "astro build", + "build:bundle": "astro build", + "clean": "npx rimraf node_modules,pnpm-lock.yaml", + "dev": "astro dev", + "preview": "astro preview", + "start": "astro dev", + "test:prod": "pnpm -v", + "test:dev": "pnpm -v", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm -v" + }, + "dependencies": { + "@astrojs/cloudflare": "8.1.0", + "@sentry/astro": "latest || *", + "astro": "4.1.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/public/favicon.svg b/dev-packages/e2e-tests/test-applications/cloudflare-astro/public/favicon.svg new file mode 100644 index 000000000000..f157bd1c5e28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/sentry.client.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-astro/sentry.client.mjs new file mode 100644 index 000000000000..3e3a697a2903 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/sentry.client.mjs @@ -0,0 +1,5 @@ +import * as Sentry from '@sentry/astro'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/sentry.server.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-astro/sentry.server.mjs new file mode 100644 index 000000000000..3e3a697a2903 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/sentry.server.mjs @@ -0,0 +1,5 @@ +import * as Sentry from '@sentry/astro'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/components/Card.astro b/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/components/Card.astro new file mode 100644 index 000000000000..bd6d5971ebf3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/components/Card.astro @@ -0,0 +1,61 @@ +--- +interface Props { + title: string; + body: string; + href: string; +} + +const { href, title, body } = Astro.props; +--- + + + diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/env.d.ts new file mode 100644 index 000000000000..f964fe0cffd8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/layouts/Layout.astro b/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/layouts/Layout.astro new file mode 100644 index 000000000000..7b552be19bca --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/layouts/Layout.astro @@ -0,0 +1,51 @@ +--- +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + + + {title} + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/pages/index.astro b/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/pages/index.astro new file mode 100644 index 000000000000..fb6262872d0e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/src/pages/index.astro @@ -0,0 +1,123 @@ +--- +import Layout from '../layouts/Layout.astro'; +import Card from '../components/Card.astro'; +--- + + +
+ +

Welcome to Astro

+

+ To get started, open the directory src/pages in your project.
+ Code Challenge: Tweak the "Welcome to Astro" message above. +

+ +
+
+ + diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts b/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts index 4b7b8703332a..d1891f5add90 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts @@ -5,8 +5,10 @@ import type { NextApiRequest, NextApiResponse } from 'next'; export default function handler(req: NextApiRequest, res: NextApiResponse) { // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); - Sentry.getCurrentHub().getScope().setSpan(transaction); + // eslint-disable-next-line deprecation/deprecation + Sentry.getCurrentScope().setSpan(transaction); + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild(); span.end(); 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 ca4f4cb0d1ff..f975ec49e606 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 @@ -130,9 +130,9 @@ test('Should send a transaction for instrumented server actions', async ({ page await page.getByText('Run Action').click(); expect(await serverComponentTransactionPromise).toBeDefined(); - expect((await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_form_data']).toEqual( - expect.objectContaining({ 'some-text-value': 'some-default-value' }), - ); + expect( + (await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_form_data.some-text-value'], + ).toEqual('some-default-value'); expect((await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_result']).toEqual({ city: 'Vienna', }); diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts index 84a973219aa8..cb3685ae8939 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts @@ -68,7 +68,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { span_id: expect.any(String), status: 'ok', tags: { - 'http.status_code': 200, + 'http.status_code': '200', }, trace_id: traceId, origin: 'auto.http.otel.http', @@ -91,7 +91,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { span_id: expect.any(String), status: 'ok', tags: { - 'http.status_code': 200, + 'http.status_code': '200', }, trace_id: traceId, origin: 'auto.http.otel.http', @@ -161,7 +161,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { span_id: expect.any(String), status: 'ok', tags: { - 'http.status_code': 200, + 'http.status_code': '200', }, trace_id: traceId, origin: 'auto.http.otel.http', @@ -184,7 +184,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { span_id: expect.any(String), status: 'ok', tags: { - 'http.status_code': 200, + 'http.status_code': '200', }, trace_id: traceId, origin: 'auto.http.otel.http', diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts index a6816d4543c3..e65a4c35b042 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts @@ -33,7 +33,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { span_id: expect.any(String), status: 'ok', tags: { - 'http.status_code': 200, + 'http.status_code': '200', }, trace_id: expect.any(String), origin: 'auto.http.otel.http', @@ -85,7 +85,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { }, ], tags: { - 'http.status_code': 200, + 'http.status_code': '200', }, transaction: 'GET /test-transaction', type: 'transaction', diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts index 8ab1935c723e..43c952b23594 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts @@ -38,6 +38,7 @@ app.get('/test-transaction', async function (req, res) { const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); Sentry.getCurrentScope().setSpan(transaction); + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild(); span.end(); diff --git a/dev-packages/node-integration-tests/.eslintrc.js b/dev-packages/node-integration-tests/.eslintrc.js index 6c8a493dccb3..df04aa267446 100644 --- a/dev-packages/node-integration-tests/.eslintrc.js +++ b/dev-packages/node-integration-tests/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { extends: ['../../.eslintrc.js'], overrides: [ { - files: ['utils/**/*.ts'], + files: ['utils/**/*.ts', 'src/**/*.ts'], parserOptions: { project: ['tsconfig.json'], sourceType: 'module', diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index c186c7fbfeb6..d9f292a9be4f 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -1,12 +1,19 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "7.92.0", + "version": "7.93.0", "license": "MIT", "engines": { "node": ">=10" }, "private": true, + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/src/index.d.ts", "scripts": { + "build": "run-s build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "tsc -p tsconfig.types.json", "clean": "rimraf -g **/node_modules", "prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)", "prisma:init:new": "(cd suites/tracing-new/prisma-orm && ts-node ./setup.ts)", @@ -15,12 +22,14 @@ "type-check": "tsc", "pretest": "run-s --silent prisma:init prisma:init:new", "test": "ts-node ./utils/run-tests.ts", + "jest": "jest --config ./jest.config.js", "test:watch": "yarn test --watch" }, "dependencies": { "@prisma/client": "3.15.2", - "@sentry/node": "7.92.0", - "@sentry/tracing": "7.92.0", + "@sentry/node": "7.93.0", + "@sentry/tracing": "7.93.0", + "@sentry/types": "7.93.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", diff --git a/dev-packages/node-integration-tests/rollup.npm.config.mjs b/dev-packages/node-integration-tests/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/dev-packages/node-integration-tests/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/dev-packages/node-integration-tests/src/index.ts b/dev-packages/node-integration-tests/src/index.ts new file mode 100644 index 000000000000..423b1ac3d93f --- /dev/null +++ b/dev-packages/node-integration-tests/src/index.ts @@ -0,0 +1,31 @@ +import type { AddressInfo } from 'net'; +import type { BaseTransportOptions, Envelope, Transport, TransportMakeRequestResponse } from '@sentry/types'; +import type { Express } from 'express'; + +/** + * Debug logging transport + */ +export function loggingTransport(_options: BaseTransportOptions): Transport { + return { + send(request: Envelope): Promise { + // eslint-disable-next-line no-console + console.log(JSON.stringify(request)); + return Promise.resolve({ statusCode: 200 }); + }, + flush(): PromiseLike { + return Promise.resolve(true); + }, + }; +} + +/** + * Starts an express server and sends the port to the runner + */ +export function startExpressServerAndSendPortToRunner(app: Express): void { + const server = app.listen(0, () => { + const address = server.address() as AddressInfo; + + // eslint-disable-next-line no-console + console.log(`{"port":${address.port}}`); + }); +} diff --git a/dev-packages/node-integration-tests/suites/anr/basic-session.js b/dev-packages/node-integration-tests/suites/anr/basic-session.js index 03c8c94fdadf..fe4190c8cc46 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic-session.js +++ b/dev-packages/node-integration-tests/suites/anr/basic-session.js @@ -11,11 +11,11 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, - integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 200 })], + integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 100 })], }); function longWork() { - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 20; i++) { const salt = crypto.randomBytes(128).toString('base64'); // eslint-disable-next-line no-unused-vars const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); diff --git a/dev-packages/node-integration-tests/suites/anr/basic.js b/dev-packages/node-integration-tests/suites/anr/basic.js index 5e0323e2c6c5..097dec6c925c 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.js +++ b/dev-packages/node-integration-tests/suites/anr/basic.js @@ -12,11 +12,11 @@ Sentry.init({ release: '1.0', debug: true, autoSessionTracking: false, - integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 200 })], + integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 100 })], }); function longWork() { - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 20; i++) { const salt = crypto.randomBytes(128).toString('base64'); // eslint-disable-next-line no-unused-vars const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); diff --git a/dev-packages/node-integration-tests/suites/anr/basic.mjs b/dev-packages/node-integration-tests/suites/anr/basic.mjs index 17c8a2d460df..43a8d02a41ac 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.mjs +++ b/dev-packages/node-integration-tests/suites/anr/basic.mjs @@ -12,11 +12,11 @@ Sentry.init({ release: '1.0', debug: true, autoSessionTracking: false, - integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 200 })], + integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 100 })], }); function longWork() { - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 20; i++) { const salt = crypto.randomBytes(128).toString('base64'); // eslint-disable-next-line no-unused-vars const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); diff --git a/dev-packages/node-integration-tests/suites/anr/forked.js b/dev-packages/node-integration-tests/suites/anr/forked.js index 5e0323e2c6c5..097dec6c925c 100644 --- a/dev-packages/node-integration-tests/suites/anr/forked.js +++ b/dev-packages/node-integration-tests/suites/anr/forked.js @@ -12,11 +12,11 @@ Sentry.init({ release: '1.0', debug: true, autoSessionTracking: false, - integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 200 })], + integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 100 })], }); function longWork() { - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 20; i++) { const salt = crypto.randomBytes(128).toString('base64'); // eslint-disable-next-line no-unused-vars const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); diff --git a/dev-packages/node-integration-tests/suites/anr/legacy.js b/dev-packages/node-integration-tests/suites/anr/legacy.js index 46b6e1437b10..f91db4bec054 100644 --- a/dev-packages/node-integration-tests/suites/anr/legacy.js +++ b/dev-packages/node-integration-tests/suites/anr/legacy.js @@ -15,9 +15,9 @@ Sentry.init({ }); // eslint-disable-next-line deprecation/deprecation -Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => { +Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 100 }).then(() => { function longWork() { - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 20; i++) { const salt = crypto.randomBytes(128).toString('base64'); // eslint-disable-next-line no-unused-vars const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index 2c2e513559cf..5edbe6dd2f78 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -1,188 +1,105 @@ -import * as childProcess from 'child_process'; -import * as path from 'path'; -import type { Event } from '@sentry/node'; -import type { SerializedSession } from '@sentry/types'; import { conditionalTest } from '../../utils'; - -/** The output will contain logging so we need to find the line that parses as JSON */ -function parseJsonLines(input: string, expected: number): T { - const results = input - .split('\n') - .map(line => { - const trimmed = line.startsWith('[ANR Worker] ') ? line.slice(13) : line; - try { - return JSON.parse(trimmed) as T; - } catch { - return undefined; - } - }) - .filter(a => a) as T; - - expect(results.length).toEqual(expected); - - return results; -} +import { createRunner } from '../../utils/runner'; + +const EXPECTED_ANR_EVENT = { + // Ensure we have context + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + device: { + arch: expect.any(String), + }, + app: { + app_start_time: expect.any(String), + }, + os: { + name: expect.any(String), + }, + culture: { + timezone: expect.any(String), + }, + }, + // and an exception that is our ANR + exception: { + values: [ + { + type: 'ApplicationNotResponding', + value: 'Application Not Responding for at least 100 ms', + mechanism: { type: 'ANR' }, + stacktrace: { + frames: expect.arrayContaining([ + { + colno: expect.any(Number), + lineno: expect.any(Number), + filename: expect.any(String), + function: '?', + in_app: true, + }, + { + colno: expect.any(Number), + lineno: expect.any(Number), + filename: expect.any(String), + function: 'longWork', + in_app: true, + }, + ]), + }, + }, + ], + }, +}; conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => { - test('CJS', done => { - expect.assertions(13); - - const testScriptPath = path.resolve(__dirname, 'basic.js'); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const [event] = parseJsonLines<[Event]>(stdout, 1); - - expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); - expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); - expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); - expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); - - expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); - expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); - - expect(event.contexts?.trace?.trace_id).toBeDefined(); - expect(event.contexts?.trace?.span_id).toBeDefined(); - - expect(event.contexts?.device?.arch).toBeDefined(); - expect(event.contexts?.app?.app_start_time).toBeDefined(); - expect(event.contexts?.os?.name).toBeDefined(); - expect(event.contexts?.culture?.timezone).toBeDefined(); - - done(); - }); - }); - + // TODO (v8): Remove this old API and this test test('Legacy API', done => { - // TODO (v8): Remove this old API and this test - expect.assertions(9); - - const testScriptPath = path.resolve(__dirname, 'legacy.js'); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const [event] = parseJsonLines<[Event]>(stdout, 1); - - expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); - expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); - expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); - expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); - - expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); - expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); - - expect(event.contexts?.trace?.trace_id).toBeDefined(); - expect(event.contexts?.trace?.span_id).toBeDefined(); + createRunner(__dirname, 'legacy.js').expect({ event: EXPECTED_ANR_EVENT }).start(done); + }); - done(); - }); + test('CJS', done => { + createRunner(__dirname, 'basic.js').expect({ event: EXPECTED_ANR_EVENT }).start(done); }); test('ESM', done => { - expect.assertions(7); - - const testScriptPath = path.resolve(__dirname, 'basic.mjs'); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const [event] = parseJsonLines<[Event]>(stdout, 1); - - expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); - expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); - expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); - expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThanOrEqual(4); - expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); - expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); - - done(); - }); + createRunner(__dirname, 'basic.mjs').expect({ event: EXPECTED_ANR_EVENT }).start(done); }); test('With --inspect', done => { - expect.assertions(7); - - const testScriptPath = path.resolve(__dirname, 'basic.js'); - - childProcess.exec(`node --inspect ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const [event] = parseJsonLines<[Event]>(stdout, 1); - - expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); - expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); - expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); - expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); - - expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); - expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); - - done(); - }); + createRunner(__dirname, 'basic.mjs').withFlags('--inspect').expect({ event: EXPECTED_ANR_EVENT }).start(done); }); test('should exit', done => { - const testScriptPath = path.resolve(__dirname, 'should-exit.js'); - let hasClosed = false; + const runner = createRunner(__dirname, 'should-exit.js').start(); setTimeout(() => { - expect(hasClosed).toBe(true); + expect(runner.childHasExited()).toBe(true); done(); }, 5_000); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, () => { - hasClosed = true; - }); }); test('should exit forced', done => { - const testScriptPath = path.resolve(__dirname, 'should-exit-forced.js'); - let hasClosed = false; + const runner = createRunner(__dirname, 'should-exit-forced.js').start(); setTimeout(() => { - expect(hasClosed).toBe(true); + expect(runner.childHasExited()).toBe(true); done(); }, 5_000); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, () => { - hasClosed = true; - }); }); test('With session', done => { - expect.assertions(9); - - const testScriptPath = path.resolve(__dirname, 'basic-session.js'); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const [session, event] = parseJsonLines<[SerializedSession, Event]>(stdout, 2); - - expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); - expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); - expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); - expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); - - expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); - expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); - - expect(session.status).toEqual('abnormal'); - expect(session.abnormal_mechanism).toEqual('anr_foreground'); - - done(); - }); + createRunner(__dirname, 'basic-session.js') + .expect({ + session: { + status: 'abnormal', + abnormal_mechanism: 'anr_foreground', + }, + }) + .expect({ event: EXPECTED_ANR_EVENT }) + .start(done); }); test('from forked process', done => { - expect.assertions(7); - - const testScriptPath = path.resolve(__dirname, 'forker.js'); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const [event] = parseJsonLines<[Event]>(stdout, 1); - - expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); - expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); - expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); - expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); - - expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); - expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); - - done(); - }); + createRunner(__dirname, 'forker.js').expect({ event: EXPECTED_ANR_EVENT }).start(done); }); }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts index 31e5580fe56a..5f3f4803ed34 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts @@ -26,8 +26,10 @@ app.use(Sentry.Handlers.tracingHandler()); app.use(cors()); app.get('/test/express', (_req, res) => { - const transaction = Sentry.getCurrentHub().getScope().getTransaction(); + // eslint-disable-next-line deprecation/deprecation + const transaction = Sentry.getCurrentScope().getTransaction(); if (transaction) { + // eslint-disable-next-line deprecation/deprecation transaction.traceId = '86f39e84263a4de99c326acab3bfe3bd'; } const headers = http.get('http://somewhere.not.sentry/').getHeaders(); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts index 061a7815ca75..214286282691 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts @@ -1,4 +1,5 @@ import http from 'http'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as Sentry from '@sentry/node'; import * as Tracing from '@sentry/tracing'; import cors from 'cors'; @@ -29,10 +30,12 @@ app.use(Sentry.Handlers.tracingHandler()); app.use(cors()); app.get('/test/express', (_req, res) => { - const transaction = Sentry.getCurrentHub().getScope().getTransaction(); + // eslint-disable-next-line deprecation/deprecation + const transaction = Sentry.getCurrentScope().getTransaction(); if (transaction) { + // eslint-disable-next-line deprecation/deprecation transaction.traceId = '86f39e84263a4de99c326acab3bfe3bd'; - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } const headers = http.get('http://somewhere.not.sentry/').getHeaders(); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/server.ts b/dev-packages/node-integration-tests/suites/express/tracing/server.ts index 1c56a81fef98..dfd6df7526fd 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/server.ts +++ b/dev-packages/node-integration-tests/suites/express/tracing/server.ts @@ -1,3 +1,4 @@ +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; import cors from 'cors'; import express from 'express'; @@ -11,6 +12,7 @@ Sentry.init({ tracePropagationTargets: [/^(?!.*test).*$/], integrations: [new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Express({ app })], tracesSampleRate: 1.0, + transport: loggingTransport, }); app.use(Sentry.Handlers.requestHandler()); @@ -36,4 +38,4 @@ app.get(['/test/arr/:id', /\/test\/arr[0-9]*\/required(path)?(\/optionalPath)?\/ app.use(Sentry.Handlers.errorHandler()); -export default app; +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/test.ts index e391ca881b30..089a2ac16edf 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/express/tracing/test.ts @@ -1,89 +1,95 @@ -import { TestEnv, assertSentryTransaction } from '../../../utils/index'; +import { createRunner } from '../../../utils/runner'; -test('should create and send transactions for Express routes and spans for middlewares.', async () => { - const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`); - const envelope = await env.getEnvelopeRequest({ url: `${env.url}/express`, envelopeType: 'transaction' }); - - expect(envelope).toHaveLength(3); - - assertSentryTransaction(envelope[2], { - contexts: { - trace: { - data: { - url: '/test/express', - 'http.response.status_code': 200, - }, - op: 'http.server', - status: 'ok', - tags: { - 'http.status_code': '200', +test('should create and send transactions for Express routes and spans for middlewares.', done => { + createRunner(__dirname, 'server.ts') + .expect({ + transaction: { + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + url: '/test/express', + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + tags: { + 'http.status_code': '200', + }, + }, }, + spans: [ + expect.objectContaining({ + description: 'corsMiddleware', + op: 'middleware.express.use', + }), + ], }, - }, - spans: [ - { - description: 'corsMiddleware', - op: 'middleware.express.use', - }, - ], - }); + }) + .start(done) + .makeRequest('get', '/test/express'); }); -test('should set a correct transaction name for routes specified in RegEx', async () => { - const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`); - const envelope = await env.getEnvelopeRequest({ url: `${env.url}/regex`, envelopeType: 'transaction' }); - - expect(envelope).toHaveLength(3); - - assertSentryTransaction(envelope[2], { - transaction: 'GET /\\/test\\/regex/', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - url: '/test/regex', - 'http.response.status_code': 200, +test('should set a correct transaction name for routes specified in RegEx', done => { + createRunner(__dirname, 'server.ts') + .expect({ + transaction: { + transaction: 'GET /\\/test\\/regex/', + transaction_info: { + source: 'route', }, - op: 'http.server', - status: 'ok', - tags: { - 'http.status_code': '200', + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + data: { + url: '/test/regex', + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + tags: { + 'http.status_code': '200', + }, + }, }, }, - }, - }); + }) + .start(done) + .makeRequest('get', '/test/regex'); }); test.each([['array1'], ['array5']])( 'should set a correct transaction name for routes consisting of arrays of routes', - async segment => { - const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`); - const envelope = await env.getEnvelopeRequest({ url: `${env.url}/${segment}`, envelopeType: 'transaction' }); - - expect(envelope).toHaveLength(3); - - assertSentryTransaction(envelope[2], { - transaction: 'GET /test/array1,/\\/test\\/array[2-9]', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - url: `/test/${segment}`, - 'http.response.status_code': 200, + ((segment: string, done: () => void) => { + createRunner(__dirname, 'server.ts') + .expect({ + transaction: { + transaction: 'GET /test/array1,/\\/test\\/array[2-9]', + transaction_info: { + source: 'route', }, - op: 'http.server', - status: 'ok', - tags: { - 'http.status_code': '200', + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + data: { + url: `/test/${segment}`, + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + tags: { + 'http.status_code': '200', + }, + }, }, }, - }, - }); - }, + }) + .start(done) + .makeRequest('get', `/test/${segment}`); + }) as any, ); test.each([ @@ -95,29 +101,31 @@ test.each([ ['arr55/required/lastParam'], ['arr/requiredPath/optionalPath/'], ['arr/requiredPath/optionalPath/lastParam'], -])('should handle more complex regexes in route arrays correctly', async segment => { - const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`); - const envelope = await env.getEnvelopeRequest({ url: `${env.url}/${segment}`, envelopeType: 'transaction' }); - - expect(envelope).toHaveLength(3); - - assertSentryTransaction(envelope[2], { - transaction: 'GET /test/arr/:id,/\\/test\\/arr[0-9]*\\/required(path)?(\\/optionalPath)?\\/(lastParam)?', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - url: `/test/${segment}`, - 'http.response.status_code': 200, +])('should handle more complex regexes in route arrays correctly', ((segment: string, done: () => void) => { + createRunner(__dirname, 'server.ts') + .expect({ + transaction: { + transaction: 'GET /test/arr/:id,/\\/test\\/arr[0-9]*\\/required(path)?(\\/optionalPath)?\\/(lastParam)?', + transaction_info: { + source: 'route', }, - op: 'http.server', - status: 'ok', - tags: { - 'http.status_code': '200', + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + data: { + url: `/test/${segment}`, + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + tags: { + 'http.status_code': '200', + }, + }, }, }, - }, - }); -}); + }) + .start(done) + .makeRequest('get', `/test/${segment}`); +}) as any); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js index 08a8d81383a1..7c86004da43b 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js @@ -1,13 +1,11 @@ /* eslint-disable no-unused-vars */ const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, - beforeSend: event => { - // eslint-disable-next-line no-console - console.log(JSON.stringify(event)); - }, + transport: loggingTransport, }); class Some { diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs index 3fbf2ae69df7..37e5966bc575 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs @@ -1,13 +1,11 @@ /* eslint-disable no-unused-vars */ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, - beforeSend: event => { - // eslint-disable-next-line no-console - console.log(JSON.stringify(event)); - }, + transport: loggingTransport, }); class Some { diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-memory-test.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-memory-test.js new file mode 100644 index 000000000000..7aa9feb9ae2a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-memory-test.js @@ -0,0 +1,43 @@ +/* eslint-disable no-unused-vars */ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + includeLocalVariables: true, + transport: loggingTransport, + // Stop the rate limiting from kicking in + integrations: [new Sentry.Integrations.LocalVariables({ maxExceptionsPerSecond: 10000000 })], +}); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + + const ty = new Some(); + + ty.two(name); +} + +// Every millisecond cause a caught exception +setInterval(() => { + try { + one('some name'); + } catch (e) { + // + } +}, 1); + +// Every second send a memory usage update to parent process +setInterval(() => { + process.send({ memUsage: process.memoryUsage() }); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js index a579a9cf5ff0..4a16ad89b5aa 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js @@ -1,13 +1,11 @@ /* eslint-disable no-unused-vars */ const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, - beforeSend: event => { - // eslint-disable-next-line no-console - console.log(JSON.stringify(event)); - }, + transport: loggingTransport, }); process.on('uncaughtException', () => { diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js index e9f189647e1a..f01e33a9cafa 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js @@ -1,12 +1,10 @@ /* eslint-disable no-unused-vars */ const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - beforeSend: event => { - // eslint-disable-next-line no-console - console.log(JSON.stringify(event)); - }, + transport: loggingTransport, }); process.on('uncaughtException', () => { diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index 0b542c19c629..75e95f09860c 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -1,112 +1,91 @@ import * as childProcess from 'child_process'; import * as path from 'path'; -import type { Event } from '@sentry/node'; - import { conditionalTest } from '../../../utils'; +import { createRunner } from '../../../utils/runner'; + +const EXPECTED_LOCAL_VARIABLES_EVENT = { + exception: { + values: [ + { + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + function: 'one', + vars: { + name: 'some name', + arr: [1, '2', null], + obj: { name: 'some name', num: 5 }, + ty: '', + }, + }), + expect.objectContaining({ + function: 'Some.two', + vars: { name: 'some name' }, + }), + ]), + }, + }, + ], + }, +}; conditionalTest({ min: 18 })('LocalVariables integration', () => { test('Should not include local variables by default', done => { - expect.assertions(2); - - const testScriptPath = path.resolve(__dirname, 'no-local-variables.js'); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const event = JSON.parse(stdout) as Event; - - const frames = event.exception?.values?.[0].stacktrace?.frames || []; - const lastFrame = frames[frames.length - 1]; - - expect(lastFrame.vars).toBeUndefined(); - - const penultimateFrame = frames[frames.length - 2]; - - expect(penultimateFrame.vars).toBeUndefined(); - - done(); - }); + createRunner(__dirname, 'no-local-variables.js') + .ignore('session') + .expect({ + event: event => { + for (const frame of event.exception?.values?.[0].stacktrace?.frames || []) { + expect(frame.vars).toBeUndefined(); + } + }, + }) + .start(done); }); test('Should include local variables when enabled', done => { - expect.assertions(4); - - const testScriptPath = path.resolve(__dirname, 'local-variables.js'); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const event = JSON.parse(stdout) as Event; - - const frames = event.exception?.values?.[0].stacktrace?.frames || []; - const lastFrame = frames[frames.length - 1]; - - expect(lastFrame.function).toBe('Some.two'); - expect(lastFrame.vars).toEqual({ name: 'some name' }); - - const penultimateFrame = frames[frames.length - 2]; - - expect(penultimateFrame.function).toBe('one'); - expect(penultimateFrame.vars).toEqual({ - name: 'some name', - arr: [1, '2', null], - obj: { name: 'some name', num: 5 }, - ty: '', - }); - - done(); - }); + createRunner(__dirname, 'local-variables.js') + .ignore('session') + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start(done); }); test('Should include local variables with ESM', done => { - expect.assertions(4); - - const testScriptPath = path.resolve(__dirname, 'local-variables-caught.mjs'); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const event = JSON.parse(stdout) as Event; - - const frames = event.exception?.values?.[0].stacktrace?.frames || []; - const lastFrame = frames[frames.length - 1]; - - expect(lastFrame.function).toBe('Some.two'); - expect(lastFrame.vars).toEqual({ name: 'some name' }); - - const penultimateFrame = frames[frames.length - 2]; - - expect(penultimateFrame.function).toBe('one'); - expect(penultimateFrame.vars).toEqual({ - name: 'some name', - arr: [1, '2', null], - obj: { name: 'some name', num: 5 }, - ty: '', - }); - - done(); - }); + createRunner(__dirname, 'local-variables-caught.mjs') + .ignore('session') + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start(done); }); test('Includes local variables for caught exceptions when enabled', done => { - expect.assertions(4); - - const testScriptPath = path.resolve(__dirname, 'local-variables-caught.js'); - - childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const event = JSON.parse(stdout) as Event; + createRunner(__dirname, 'local-variables-caught.js') + .ignore('session') + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start(done); + }); - const frames = event.exception?.values?.[0].stacktrace?.frames || []; - const lastFrame = frames[frames.length - 1]; + test('Should not leak memory', done => { + const testScriptPath = path.resolve(__dirname, 'local-variables-memory-test.js'); - expect(lastFrame.function).toBe('Some.two'); - expect(lastFrame.vars).toEqual({ name: 'some name' }); + const child = childProcess.spawn('node', [testScriptPath], { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + }); - const penultimateFrame = frames[frames.length - 2]; + let reportedCount = 0; - expect(penultimateFrame.function).toBe('one'); - expect(penultimateFrame.vars).toEqual({ - name: 'some name', - arr: [1, '2', null], - obj: { name: 'some name', num: 5 }, - ty: '', - }); + child.on('message', msg => { + reportedCount++; + const rssMb = msg.memUsage.rss / 1024 / 1024; + // We shouldn't use more than 100MB of memory + expect(rssMb).toBeLessThan(100); + }); + // Wait for 20 seconds + setTimeout(() => { + // Ensure we've had memory usage reported at least 15 times + expect(reportedCount).toBeGreaterThan(15); + child.kill(); done(); - }); + }, 20000); }); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts index 97fc6874770f..f82fe81d969a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts @@ -10,6 +10,7 @@ Sentry.init({ // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); +// eslint-disable-next-line deprecation/deprecation const span_1 = transaction.startChild({ op: 'span_1', data: { @@ -23,16 +24,20 @@ for (let i = 0; i < 2000; i++); span_1.end(); // span_2 doesn't finish +// eslint-disable-next-line deprecation/deprecation transaction.startChild({ op: 'span_2' }); for (let i = 0; i < 4000; i++); +// eslint-disable-next-line deprecation/deprecation const span_3 = transaction.startChild({ op: 'span_3' }); for (let i = 0; i < 4000; i++); // span_4 is the child of span_3 but doesn't finish. +// eslint-disable-next-line deprecation/deprecation span_3.startChild({ op: 'span_4', data: { qux: 'quux' } }); // span_5 is another child of span_3 but finishes. +// eslint-disable-next-line deprecation/deprecation span_3.startChild({ op: 'span_5' }).end(); // span_3 also finishes diff --git a/dev-packages/node-integration-tests/suites/sessions/server.ts b/dev-packages/node-integration-tests/suites/sessions/server.ts index 8329dffa2bc5..1a92ced15a19 100644 --- a/dev-packages/node-integration-tests/suites/sessions/server.ts +++ b/dev-packages/node-integration-tests/suites/sessions/server.ts @@ -14,7 +14,7 @@ app.use(Sentry.Handlers.requestHandler()); // ### Taken from manual tests ### // Hack that resets the 60s default flush interval, and replaces it with just a one second interval -const flusher = (Sentry.getCurrentHub()?.getClient() as Sentry.NodeClient)['_sessionFlusher'] as SessionFlusher; +const flusher = (Sentry.getClient() as Sentry.NodeClient)['_sessionFlusher'] as SessionFlusher; let flusherIntervalId = flusher && flusher['_intervalId']; diff --git a/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts index 6e9ea3a5f097..1584274bce7d 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts @@ -30,6 +30,7 @@ const server = new ApolloServer({ // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts index 63949c64d531..67d8e13750de 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts @@ -22,6 +22,7 @@ async function run(): Promise { op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts index a1b2343e4a61..8273d4dfa96a 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts @@ -25,6 +25,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts index aa6d8ca9ade3..2b06970b5243 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts @@ -25,16 +25,17 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); const query = connection.query('SELECT 1 + 1 AS solution'); const query2 = connection.query('SELECT NOW()', ['1', '2']); query.on('end', () => { - transaction.setTag('result_done', 'yes'); + transaction.setAttribute('result_done', 'yes'); query2.on('end', () => { - transaction.setTag('result_done2', 'yes'); + transaction.setAttribute('result_done2', 'yes'); // Wait a bit to ensure the queries completed setTimeout(() => { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts index 0f6dee99d59b..f83e4297b8ba 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts @@ -7,11 +7,15 @@ test('should auto-instrument `mysql` package when using query without callback', expect(envelope).toHaveLength(3); assertSentryTransaction(envelope[2], { - transaction: 'Test Transaction', - tags: { - result_done: 'yes', - result_done2: 'yes', + contexts: { + trace: { + data: { + result_done: 'yes', + result_done2: 'yes', + }, + }, }, + transaction: 'Test Transaction', spans: [ { description: 'SELECT 1 + 1 AS solution', diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts index 9a4b2dc4141a..d88b2d1c8d24 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts @@ -19,6 +19,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts index ad90c387ed20..47fb37e054f7 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts @@ -14,6 +14,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); const client = new pg.Client(); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts index 26c3b81f6e44..6191dbf31d75 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts @@ -19,6 +19,7 @@ async function run(): Promise { op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts index 2a8a34dae4f9..600b5ef71038 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts @@ -13,6 +13,7 @@ Sentry.init({ // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); http.get('http://match-this-url.com/api/v0'); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts index aac11a641032..6a699fa07af7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts @@ -32,6 +32,7 @@ const server = new ApolloServer({ // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts index f11e7f39d923..51359ac726da 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts @@ -23,6 +23,7 @@ async function run(): Promise { op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts index de77c5fa7c8a..ce53d776fe54 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts @@ -26,6 +26,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts index 1963c61fc31b..f9bfa0de0294 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts @@ -15,6 +15,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); const client = new pg.Client(); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts index 087d5ce860f2..b5003141caec 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts @@ -21,6 +21,7 @@ async function run(): Promise { op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts index f1e395992b46..8ecb7ed3cc61 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts @@ -15,6 +15,7 @@ Sentry.init({ // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); http.get('http://match-this-url.com/api/v0'); diff --git a/dev-packages/node-integration-tests/tsconfig.json b/dev-packages/node-integration-tests/tsconfig.json index 782d8f9c517f..92db70d5ca09 100644 --- a/dev-packages/node-integration-tests/tsconfig.json +++ b/dev-packages/node-integration-tests/tsconfig.json @@ -1,11 +1,11 @@ { "extends": "../../tsconfig.json", - "include": ["utils/**/*.ts"], + "include": ["utils/**/*.ts", "src/**/*.ts"], "compilerOptions": { // package-specific options "esModuleInterop": true, - "types": ["node"] + "types": ["node", "jest"] } } diff --git a/dev-packages/node-integration-tests/tsconfig.types.json b/dev-packages/node-integration-tests/tsconfig.types.json new file mode 100644 index 000000000000..65455f66bd75 --- /dev/null +++ b/dev-packages/node-integration-tests/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + } +} diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts new file mode 100644 index 000000000000..2b89c9deb46f --- /dev/null +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -0,0 +1,226 @@ +import { spawn } from 'child_process'; +import { join } from 'path'; +import type { Envelope, EnvelopeItemType, Event, SerializedSession } from '@sentry/types'; +import axios from 'axios'; + +export function assertSentryEvent(actual: Event, expected: Event): void { + expect(actual).toMatchObject({ + event_id: expect.any(String), + ...expected, + }); +} + +export function assertSentrySession(actual: SerializedSession, expected: Partial): void { + expect(actual).toMatchObject({ + sid: expect.any(String), + ...expected, + }); +} + +export function assertSentryTransaction(actual: Event, expected: Partial): void { + expect(actual).toMatchObject({ + event_id: expect.any(String), + timestamp: expect.anything(), + start_timestamp: expect.anything(), + spans: expect.any(Array), + type: 'transaction', + ...expected, + }); +} + +type Expected = + | { + event: Partial | ((event: Event) => void); + } + | { + transaction: Partial | ((event: Event) => void); + } + | { + session: Partial | ((event: SerializedSession) => void); + }; + +/** */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function createRunner(...paths: string[]) { + const testPath = join(...paths); + + const expectedEnvelopes: Expected[] = []; + const flags: string[] = []; + const ignored: EnvelopeItemType[] = []; + let hasExited = false; + + if (testPath.endsWith('.ts')) { + flags.push('-r', 'ts-node/register'); + } + + return { + expect: function (expected: Expected) { + expectedEnvelopes.push(expected); + return this; + }, + withFlags: function (...args: string[]) { + flags.push(...args); + return this; + }, + ignore: function (...types: EnvelopeItemType[]) { + ignored.push(...types); + return this; + }, + start: function (done?: (e?: unknown) => void) { + const expectedEnvelopeCount = expectedEnvelopes.length; + let envelopeCount = 0; + let serverPort: number | undefined; + + const child = spawn('node', [...flags, testPath]); + + child.on('close', () => { + hasExited = true; + }); + + // Pass error to done to end the test quickly + child.on('error', e => { + done?.(e); + }); + + async function waitForServerPort(timeout = 10_000): Promise { + let remaining = timeout; + while (serverPort === undefined) { + await new Promise(resolve => setTimeout(resolve, 100)); + remaining -= 100; + if (remaining < 0) { + throw new Error('Timed out waiting for server port'); + } + } + } + + /** Called after each expect callback to check if we're complete */ + function expectCallbackCalled(): void { + envelopeCount++; + if (envelopeCount === expectedEnvelopeCount) { + child.kill(); + done?.(); + } + } + + function tryParseLine(line: string): void { + // Lines can have leading '[something] [{' which we need to remove + const cleanedLine = line.replace(/^.*?] \[{"/, '[{"'); + + // See if we have a port message + if (cleanedLine.startsWith('{"port":')) { + const { port } = JSON.parse(cleanedLine) as { port: number }; + serverPort = port; + return; + } + + // Skip any lines that don't start with envelope JSON + if (!cleanedLine.startsWith('[{')) { + return; + } + + let envelope: Envelope | undefined; + try { + envelope = JSON.parse(cleanedLine) as Envelope; + } catch (_) { + return; + } + + for (const item of envelope[1]) { + const envelopeItemType = item[0].type; + + if (ignored.includes(envelopeItemType)) { + continue; + } + + const expected = expectedEnvelopes.shift(); + + // Catch any error or failed assertions and pass them to done to end the test quickly + try { + if (!expected) { + throw new Error(`No more expected envelope items but we received a '${envelopeItemType}' item`); + } + + const expectedType = Object.keys(expected)[0]; + + if (expectedType !== envelopeItemType) { + throw new Error(`Expected envelope item type '${expectedType}' but got '${envelopeItemType}'`); + } + + if ('event' in expected) { + const event = item[1] as Event; + if (typeof expected.event === 'function') { + expected.event(event); + } else { + assertSentryEvent(event, expected.event); + } + + expectCallbackCalled(); + } + + if ('transaction' in expected) { + const event = item[1] as Event; + if (typeof expected.transaction === 'function') { + expected.transaction(event); + } else { + assertSentryTransaction(event, expected.transaction); + } + + expectCallbackCalled(); + } + + if ('session' in expected) { + const session = item[1] as SerializedSession; + if (typeof expected.session === 'function') { + expected.session(session); + } else { + assertSentrySession(session, expected.session); + } + + expectCallbackCalled(); + } + } catch (e) { + done?.(e); + } + } + } + + let buffer = Buffer.alloc(0); + child.stdout.on('data', (data: Buffer) => { + // This is horribly memory inefficient but it's only for tests + buffer = Buffer.concat([buffer, data]); + + let splitIndex = -1; + while ((splitIndex = buffer.indexOf(0xa)) >= 0) { + const line = buffer.subarray(0, splitIndex).toString(); + buffer = Buffer.from(buffer.subarray(splitIndex + 1)); + tryParseLine(line); + } + }); + + return { + childHasExited: function (): boolean { + return hasExited; + }, + makeRequest: async function ( + method: 'get' | 'post', + path: string, + headers: Record = {}, + ): Promise { + try { + await waitForServerPort(); + + const url = `http://localhost:${serverPort}${path}`; + if (method === 'get') { + return (await axios.get(url, { headers })).data; + } else { + return (await axios.post(url, { headers })).data; + } + } catch (e) { + done?.(e); + return undefined; + } + }, + }; + }, + }; +} diff --git a/dev-packages/overhead-metrics/package.json b/dev-packages/overhead-metrics/package.json index 2c63ec6e4248..4c5997e4f417 100644 --- a/dev-packages/overhead-metrics/package.json +++ b/dev-packages/overhead-metrics/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "7.92.0", + "version": "7.93.0", "name": "@sentry-internal/overhead-metrics", "main": "index.js", "author": "Sentry", diff --git a/dev-packages/rollup-utils/package.json b/dev-packages/rollup-utils/package.json index f722cc79c075..b3d68715cb52 100644 --- a/dev-packages/rollup-utils/package.json +++ b/dev-packages/rollup-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/rollup-utils", - "version": "7.92.0", + "version": "7.93.0", "description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils", diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index 66f8e8c78228..9aef97dc828f 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -135,6 +135,9 @@ export function makeTerserPlugin() { // These are used by instrument.ts in utils for identifying HTML elements & events '_sentryCaptured', '_sentryId', + // For v7 backwards-compatibility we need to access txn._frozenDynamicSamplingContext + // TODO (v8): Remove this reserved word + '_frozenDynamicSamplingContext', ], }, }, diff --git a/docs/v8-new-performance-apis.md b/docs/v8-new-performance-apis.md index e7ec274bbb10..2f531858dd2c 100644 --- a/docs/v8-new-performance-apis.md +++ b/docs/v8-new-performance-apis.md @@ -50,8 +50,8 @@ below to see which things used to exist, and how they can/should be mapped going | `status` | use utility method TODO | | `sampled` | `spanIsSampled(span)` | | `startTimestamp` | `startTime` - note that this has a different format! | -| `tags` | `spanGetAttributes(span)`, or set tags on the scope | -| `data` | `spanGetAttributes(span)` | +| `tags` | use attributes, or set tags on the scope | +| `data` | `spanToJSON(span).data` | | `transaction` | ??? Removed | | `instrumenter` | Removed | | `finish()` | `end()` | @@ -72,13 +72,13 @@ In addition, a transaction has this API: | Old name | Replace with | | --------------------------- | ------------------------------------------------ | -| `name` | `spanGetName(span)` (TODO) | +| `name` | `spanToJSON(span).description` | | `trimEnd` | Removed | | `parentSampled` | `spanIsSampled(span)` & `spanContext().isRemote` | -| `metadata` | `spanGetMetadata(span)` | +| `metadata` | Use attributes instead or set on scope | | `setContext()` | Set context on scope instead | | `setMeasurement()` | ??? TODO | -| `setMetadata()` | `spanSetMetadata(span, metadata)` | +| `setMetadata()` | Use attributes instead or set on scope | | `getDynamicSamplingContext` | ??? TODO | ### Attributes vs. Data vs. Tags vs. Context diff --git a/lerna.json b/lerna.json index f63c86f26ba7..c4bbd2caa208 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "7.92.0", + "version": "7.93.0", "npmClient": "yarn" } diff --git a/package.json b/package.json index 3135af0994db..aa16add34b06 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,13 @@ "fix": "run-p fix:lerna fix:biome fix:prettier", "fix:lerna": "lerna run fix", "fix:biome": "biome check --apply .", - "fix:prettier": "prettier **/*.md **/*.css --write", + "fix:prettier": "prettier **/*.md *.md **/*.css --write", "changelog": "ts-node ./scripts/get-commit-list.ts", "link:yarn": "lerna exec yarn link", "lint": "run-p lint:lerna lint:biome lint:prettier", "lint:lerna": "lerna run lint", "lint:biome": "biome check .", - "lint:prettier": "prettier **/*.md **/*.css --check", + "lint:prettier": "prettier **/*.md *.md **/*.css --check", "validate:es5": "lerna run validate:es5", "postpublish": "lerna run --stream --concurrency 1 postpublish", "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test", @@ -97,7 +97,6 @@ "@types/chai": "^4.1.3", "@types/jest": "^27.4.1", "@types/jsdom": "^16.2.3", - "@types/mocha": "^5.2.0", "@types/node": "~10.17.0", "@types/rimraf": "^3.0.2", "@types/sinon": "^7.0.11", @@ -115,7 +114,6 @@ "karma-firefox-launcher": "^1.1.0", "lerna": "7.1.1", "madge": "4.0.2", - "mocha": "^6.1.4", "nodemon": "^2.0.16", "npm-run-all": "^4.1.5", "prettier": "^3.1.1", diff --git a/packages/angular-ivy/README.md b/packages/angular-ivy/README.md index f487ffa22707..6967e7570a82 100644 --- a/packages/angular-ivy/README.md +++ b/packages/angular-ivy/README.md @@ -215,33 +215,22 @@ export class FooterComponent implements OnInit { } ``` -You can also add your own custom spans by attaching them to the current active transaction using `getActiveTransaction` -helper. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: +You can also add your own custom spans via `startSpan()`. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: ```javascript import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { init, getActiveTransaction } from '@sentry/angular-ivy'; +import { init, startSpan } from '@sentry/angular'; import { AppModule } from './app/app.module'; // ... - -const activeTransaction = getActiveTransaction(); -const boostrapSpan = - activeTransaction && - activeTransaction.startChild({ - description: 'platform-browser-dynamic', - op: 'ui.angular.bootstrap', - }); - -platformBrowserDynamic() - .bootstrapModule(AppModule) - .then(() => console.log(`Bootstrap success`)) - .catch(err => console.error(err)); - .finally(() => { - if (bootstrapSpan) { - boostrapSpan.finish(); - } - }) +startSpan({ + name: 'platform-browser-dynamic', + op: 'ui.angular.bootstrap' + }, + async () => { + await platformBrowserDynamic().bootstrapModule(AppModule); + } +); ``` diff --git a/packages/angular-ivy/ng-package.json b/packages/angular-ivy/ng-package.json index b50faf694df3..38d9d7f5ac68 100644 --- a/packages/angular-ivy/ng-package.json +++ b/packages/angular-ivy/ng-package.json @@ -5,9 +5,10 @@ "entryFile": "src/index.ts", "umdModuleIds": { "@sentry/browser": "Sentry", - "@sentry/utils": "Sentry.util" + "@sentry/utils": "Sentry.util", + "@sentry/core": "Sentry.core" } }, - "allowedNonPeerDependencies": ["@sentry/browser", "@sentry/utils", "@sentry/types", "tslib"], + "allowedNonPeerDependencies": ["@sentry/browser", "@sentry/core", "@sentry/utils", "@sentry/types", "tslib"], "assets": ["README.md", "LICENSE"] } diff --git a/packages/angular-ivy/package.json b/packages/angular-ivy/package.json index 9d9ccd91e4db..d93d3336599e 100644 --- a/packages/angular-ivy/package.json +++ b/packages/angular-ivy/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular-ivy", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Angular with full Ivy Support", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular-ivy", @@ -21,9 +21,10 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry/browser": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/angular/README.md b/packages/angular/README.md index aa18724839a4..302b060bdb39 100644 --- a/packages/angular/README.md +++ b/packages/angular/README.md @@ -215,33 +215,22 @@ export class FooterComponent implements OnInit { } ``` -You can also add your own custom spans by attaching them to the current active transaction using `getActiveTransaction` -helper. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: +You can also add your own custom spans via `startSpan()`. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: ```javascript import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { init, getActiveTransaction } from '@sentry/angular'; +import { init, startSpan } from '@sentry/angular'; import { AppModule } from './app/app.module'; // ... - -const activeTransaction = getActiveTransaction(); -const boostrapSpan = - activeTransaction && - activeTransaction.startChild({ - description: 'platform-browser-dynamic', - op: 'ui.angular.bootstrap', - }); - -platformBrowserDynamic() - .bootstrapModule(AppModule) - .then(() => console.log(`Bootstrap success`)) - .catch(err => console.error(err)); - .finally(() => { - if (bootstrapSpan) { - boostrapSpan.finish(); - } - }) +startSpan({ + name: 'platform-browser-dynamic', + op: 'ui.angular.bootstrap' + }, + async () => { + await platformBrowserDynamic().bootstrapModule(AppModule); + } +); ``` diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json index 88df70c1c7bd..28794322dd0a 100644 --- a/packages/angular/ng-package.json +++ b/packages/angular/ng-package.json @@ -5,9 +5,10 @@ "entryFile": "src/index.ts", "umdModuleIds": { "@sentry/browser": "Sentry", - "@sentry/utils": "Sentry.util" + "@sentry/utils": "Sentry.util", + "@sentry/core": "Sentry.core" } }, - "whitelistedNonPeerDependencies": ["@sentry/browser", "@sentry/utils", "@sentry/types", "tslib"], + "whitelistedNonPeerDependencies": ["@sentry/browser", "@sentry/core", "@sentry/utils", "@sentry/types", "tslib"], "assets": ["README.md", "LICENSE"] } diff --git a/packages/angular/package.json b/packages/angular/package.json index c1cfc9765071..b88eaa47ca93 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,9 +21,10 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry/browser": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index b6f188a35d14..f7f0536463a2 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -5,6 +5,7 @@ export * from '@sentry/browser'; export { init } from './sdk'; export { createErrorHandler, SentryErrorHandler } from './errorhandler'; export { + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, // TODO `instrumentAngularRouting` is just an alias for `routingInstrumentation`; deprecate the latter at some point instrumentAngularRouting, // new name diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index e65ecd84d8df..efd2c840420b 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -8,6 +8,7 @@ import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router import { NavigationCancel, NavigationError, Router } from '@angular/router'; import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router'; import { WINDOW, getCurrentScope } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import type { Span, Transaction, TransactionContext } from '@sentry/types'; import { logger, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/utils'; import type { Observable } from 'rxjs'; @@ -39,7 +40,9 @@ export function routingInstrumentation( name: WINDOW.location.pathname, op: 'pageload', origin: 'auto.pageload.angular', - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, }); } } @@ -47,9 +50,12 @@ export function routingInstrumentation( export const instrumentAngularRouting = routingInstrumentation; /** - * Grabs active transaction off scope + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. */ export function getActiveTransaction(): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getTransaction(); } @@ -69,6 +75,7 @@ export class TraceService implements OnDestroy { } const strippedUrl = stripUrlQueryAndFragment(navigationEvent.url); + // eslint-disable-next-line deprecation/deprecation let activeTransaction = getActiveTransaction(); if (!activeTransaction && stashedStartTransactionOnLocationChange) { @@ -76,7 +83,9 @@ export class TraceService implements OnDestroy { name: strippedUrl, op: 'navigation', origin: 'auto.navigation.angular', - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, }); } @@ -84,6 +93,7 @@ export class TraceService implements OnDestroy { if (this._routingSpan) { this._routingSpan.end(); } + // eslint-disable-next-line deprecation/deprecation this._routingSpan = activeTransaction.startChild({ description: `${navigationEvent.url}`, op: ANGULAR_ROUTING_OP, @@ -115,11 +125,13 @@ export class TraceService implements OnDestroy { (event.state as unknown as RouterState & { root: ActivatedRouteSnapshot }).root, ); + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); // TODO (v8 / #5416): revisit the source condition. Do we want to make the parameterized route the default? - if (transaction && transaction.metadata.source === 'url') { + const attributes = (transaction && spanToJSON(transaction).data) || {}; + if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { transaction.updateName(route); - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } }), ); @@ -181,8 +193,10 @@ export class TraceDirective implements OnInit, AfterViewInit { this.componentName = UNKNOWN_COMPONENT; } + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation this._tracingSpan = activeTransaction.startChild({ description: `<${this.componentName}>`, op: ANGULAR_INIT_OP, @@ -223,8 +237,10 @@ export function TraceClassDecorator(): ClassDecorator { const originalOnInit = target.prototype.ngOnInit; // eslint-disable-next-line @typescript-eslint/no-explicit-any target.prototype.ngOnInit = function (...args: any[]): ReturnType { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation tracingSpan = activeTransaction.startChild({ description: `<${target.name}>`, op: ANGULAR_INIT_OP, @@ -260,8 +276,10 @@ export function TraceMethodDecorator(): MethodDecorator { // eslint-disable-next-line @typescript-eslint/no-explicit-any descriptor.value = function (...args: any[]): ReturnType { const now = timestampInSeconds(); + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation activeTransaction.startChild({ description: `<${target.constructor.name}>`, endTimestamp: now, diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index 695e3d7af564..c2406f628128 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import type { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { TraceClassDecorator, TraceDirective, TraceMethodDecorator, instrumentAngularRouting } from '../src'; import { getParameterizedRouteFromSnapshot } from '../src/tracing'; @@ -11,7 +12,14 @@ const defaultStartTransaction = (ctx: any) => { transaction = { ...ctx, updateName: jest.fn(name => (transaction.name = name)), - setMetadata: jest.fn(), + setAttribute: jest.fn(), + toJSON: () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + ...ctx.data, + ...ctx.attributes, + }, + }), }; return transaction; @@ -45,7 +53,7 @@ describe('Angular Tracing', () => { name: '/', op: 'pageload', origin: 'auto.pageload.angular', - metadata: { source: 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, }); }); }); @@ -107,11 +115,15 @@ describe('Angular Tracing', () => { const customStartTransaction = jest.fn((ctx: any) => { transaction = { ...ctx, - metadata: { - ...ctx.metadata, - source: 'custom', - }, + toJSON: () => ({ + data: { + ...ctx.data, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + }, + }), + metadata: ctx.metadata, updateName: jest.fn(name => (transaction.name = name)), + setAttribute: jest.fn(), }; return transaction; @@ -135,12 +147,12 @@ describe('Angular Tracing', () => { name: url, op: 'pageload', origin: 'auto.pageload.angular', - metadata: { source: 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, }); expect(transaction.updateName).toHaveBeenCalledTimes(0); expect(transaction.name).toEqual(url); - expect(transaction.metadata.source).toBe('custom'); + expect(transaction.toJSON().data).toEqual({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }); env.destroy(); }); @@ -326,10 +338,10 @@ describe('Angular Tracing', () => { name: url, op: 'navigation', origin: 'auto.navigation.angular', - metadata: { source: 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, }); expect(transaction.updateName).toHaveBeenCalledWith(result); - expect(transaction.setMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(transaction.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); env.destroy(); }); diff --git a/packages/astro/package.json b/packages/astro/package.json index 07b7f0712bc4..9e7ea804b3c2 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Astro", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", @@ -49,11 +49,11 @@ "astro": ">=3.x || >=4.0.0-beta" }, "dependencies": { - "@sentry/browser": "7.92.0", - "@sentry/core": "7.92.0", - "@sentry/node": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry/browser": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/node": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "@sentry/vite-plugin": "^2.8.0" }, "devDependencies": { diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 3575ee2c2929..4451979f076c 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -22,6 +22,7 @@ export { createTransport, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -45,6 +46,7 @@ export { // eslint-disable-next-line deprecation/deprecation trace, withScope, + withIsolationScope, autoDiscoverNodePerformanceMonitoringIntegrations, makeNodeTransport, defaultIntegrations, diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 142a0b0b3019..71e58811e0c8 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -84,11 +84,14 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { // Prevent Sentry from being externalized for SSR. // Cloudflare like environments have Node.js APIs are available under `node:` prefix. // Ref: https://developers.cloudflare.com/workers/runtime-apis/nodejs/ - if (config?.adapter?.name.startsWith('@astro/cloudflare')) { + if (config?.adapter?.name.startsWith('@astrojs/cloudflare')) { updateConfig({ vite: { ssr: { - noExternal: ['@sentry/astro'], + // @sentry/node is required in case we have 2 different @sentry/node + // packages installed in the same project. + // Ref: https://github.com/getsentry/sentry-javascript/issues/10121 + noExternal: ['@sentry/astro', '@sentry/node'], }, }, }); diff --git a/packages/astro/src/server/meta.ts b/packages/astro/src/server/meta.ts index bdc0d89d4f80..dd591c15d966 100644 --- a/packages/astro/src/server/meta.ts +++ b/packages/astro/src/server/meta.ts @@ -1,4 +1,9 @@ -import { getDynamicSamplingContextFromClient, spanToTraceHeader } from '@sentry/core'; +import { + getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, + getRootSpan, + spanToTraceHeader, +} from '@sentry/core'; import type { Client, Scope, Span } from '@sentry/types'; import { TRACEPARENT_REGEXP, @@ -28,12 +33,12 @@ export function getTracingMetaTags( client: Client | undefined, ): { sentryTrace: string; baggage?: string } { const { dsc, sampled, traceId } = scope.getPropagationContext(); - const transaction = span?.transaction; + const rootSpan = span && getRootSpan(span); const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); - const dynamicSamplingContext = transaction - ? transaction.getDynamicSamplingContext() + const dynamicSamplingContext = rootSpan + ? getDynamicSamplingContextFromSpan(rootSpan) : dsc ? dsc : client diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 907714d33874..d5cc61b73e95 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,6 +1,8 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { captureException, continueTrace, + getActiveSpan, getClient, getCurrentScope, runWithAsyncContext, @@ -69,7 +71,7 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH // if there is an active span, we know that this handle call is nested and hence // we don't create a new domain for it. If we created one, nested server calls would // create new transactions instead of adding a child span to the currently active span. - if (getCurrentScope().getSpan()) { + if (getActiveSpan()) { return instrumentRequest(ctx, next, handlerOptions); } return runWithAsyncContext(() => { @@ -111,6 +113,7 @@ async function instrumentRequest( try { const interpolatedRoute = interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params); + const source = interpolatedRoute ? 'route' : 'url'; // storing res in a variable instead of directly returning is necessary to // invoke the catch block if next() throws const res = await startSpan( @@ -121,12 +124,13 @@ async function instrumentRequest( origin: 'auto.http.astro', status: 'ok', metadata: { + // eslint-disable-next-line deprecation/deprecation ...traceCtx?.metadata, - source: interpolatedRoute ? 'route' : 'url', }, data: { method, url: stripUrlQueryAndFragment(ctx.url.href), + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, ...(ctx.url.search && { 'http.query': ctx.url.search }), ...(ctx.url.hash && { 'http.fragment': ctx.url.hash }), ...(options.trackHeaders && { headers: allHeaders }), diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index d2f745c55a89..161213ea6564 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -1,6 +1,7 @@ import type { BrowserClient } from '@sentry/browser'; +import { getCurrentScope } from '@sentry/browser'; import * as SentryBrowser from '@sentry/browser'; -import { BrowserTracing, SDK_VERSION, WINDOW, getClient, getCurrentHub } from '@sentry/browser'; +import { BrowserTracing, SDK_VERSION, WINDOW, getClient } from '@sentry/browser'; import { vi } from 'vitest'; import { init } from '../../../astro/src/client/sdk'; @@ -37,7 +38,7 @@ describe('Sentry client SDK', () => { }); it('sets the runtime tag on the scope', () => { - const currentScope = getCurrentHub().getScope(); + const currentScope = getCurrentScope(); // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); @@ -60,7 +61,7 @@ describe('Sentry client SDK', () => { }); const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; - const browserTracing = getClient()?.getIntegrationById('BrowserTracing'); + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); expect(browserTracing).toBeDefined(); @@ -76,7 +77,7 @@ describe('Sentry client SDK', () => { }); const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; - const browserTracing = getClient()?.getIntegrationById('BrowserTracing'); + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); expect(browserTracing).toBeUndefined(); @@ -91,7 +92,7 @@ describe('Sentry client SDK', () => { }); const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; - const browserTracing = getClient()?.getIntegrationById('BrowserTracing'); + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); expect(browserTracing).toBeUndefined(); @@ -108,7 +109,7 @@ describe('Sentry client SDK', () => { const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; - const browserTracing = getClient()?.getIntegrationById('BrowserTracing') as BrowserTracing; + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing') as BrowserTracing; const options = browserTracing.options; expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); diff --git a/packages/astro/test/server/meta.test.ts b/packages/astro/test/server/meta.test.ts index 0586507b570c..f235ad34d7ca 100644 --- a/packages/astro/test/server/meta.test.ts +++ b/packages/astro/test/server/meta.test.ts @@ -3,10 +3,17 @@ import { vi } from 'vitest'; import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta'; +const TRACE_FLAG_SAMPLED = 0x1; + const mockedSpan = { isRecording: () => true, - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', + spanContext: () => { + return { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, transaction: { getDynamicSamplingContext: () => ({ environment: 'production', @@ -25,6 +32,10 @@ const mockedScope = { describe('getTracingMetaTags', () => { it('returns the tracing tags from the span, if it is provided', () => { { + vi.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ + environment: 'production', + }); + const tags = getTracingMetaTags(mockedSpan, mockedScope, mockedClient); expect(tags).toEqual({ @@ -71,8 +82,13 @@ describe('getTracingMetaTags', () => { // @ts-expect-error - only passing a partial span object { isRecording: () => true, - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', + spanContext: () => { + return { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, transaction: undefined, }, mockedScope, @@ -84,7 +100,7 @@ describe('getTracingMetaTags', () => { }); }); - it('returns only the `sentry-trace` tag if no DSC is available', () => { + it('returns only the `sentry-trace` tag if no DSC is available without a client', () => { vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ trace_id: '', public_key: undefined, @@ -94,8 +110,13 @@ describe('getTracingMetaTags', () => { // @ts-expect-error - only passing a partial span object { isRecording: () => true, - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', + spanContext: () => { + return { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, transaction: undefined, }, mockedScope, diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 13508cebf057..9fa5bc430c90 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,5 +1,6 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as SentryNode from '@sentry/node'; -import type { Client } from '@sentry/types'; +import type { Client, Span } from '@sentry/types'; import { vi } from 'vitest'; import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware'; @@ -14,7 +15,9 @@ vi.mock('../../src/server/meta', () => ({ describe('sentryMiddleware', () => { const startSpanSpy = vi.spyOn(SentryNode, 'startSpan'); - const getSpanMock = vi.fn(() => {}); + const getSpanMock = vi.fn(() => { + return {} as Span | undefined; + }); const setUserMock = vi.fn(); beforeEach(() => { @@ -25,6 +28,7 @@ describe('sentryMiddleware', () => { getSpan: getSpanMock, } as any; }); + vi.spyOn(SentryNode, 'getActiveSpan').mockImplementation(getSpanMock); vi.spyOn(SentryNode, 'getClient').mockImplementation(() => ({}) as Client); }); @@ -57,10 +61,9 @@ describe('sentryMiddleware', () => { data: { method: 'GET', url: 'https://mydomain.io/users/123/details', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }, - metadata: { - source: 'route', - }, + metadata: {}, name: 'GET /users/[id]/details', op: 'http.server', origin: 'auto.http.astro', @@ -94,10 +97,9 @@ describe('sentryMiddleware', () => { data: { method: 'GET', url: 'http://localhost:1234/a%xx', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, - metadata: { - source: 'url', - }, + metadata: {}, name: 'GET a%xx', op: 'http.server', origin: 'auto.http.astro', @@ -159,8 +161,10 @@ describe('sentryMiddleware', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ + data: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }), metadata: { - source: 'route', dynamicSamplingContext: { release: '1.0.0', }, diff --git a/packages/astro/test/server/sdk.test.ts b/packages/astro/test/server/sdk.test.ts index 0e178f7ae45a..b132c32a03c9 100644 --- a/packages/astro/test/server/sdk.test.ts +++ b/packages/astro/test/server/sdk.test.ts @@ -1,4 +1,3 @@ -import { getCurrentHub } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; import { GLOBAL_OBJ } from '@sentry/utils'; @@ -38,7 +37,7 @@ describe('Sentry server SDK', () => { }); it('sets the runtime tag on the scope', () => { - const currentScope = getCurrentHub().getScope(); + const currentScope = SentryNode.getCurrentScope(); // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); diff --git a/packages/browser/package.json b/packages/browser/package.json index 5e336280b053..196bc7c8f65c 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -29,20 +29,21 @@ "access": "public" }, "dependencies": { - "@sentry-internal/feedback": "7.92.0", - "@sentry-internal/tracing": "7.92.0", - "@sentry/core": "7.92.0", - "@sentry/replay": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry-internal/feedback": "7.93.0", + "@sentry-internal/tracing": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/replay": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "devDependencies": { - "@sentry-internal/integration-shims": "7.92.0", + "@sentry-internal/integration-shims": "7.93.0", "@types/md5": "2.1.33", "btoa": "^1.2.1", "chai": "^4.1.2", "chokidar": "^3.0.2", "fake-indexeddb": "^4.0.1", + "mocha": "^6.1.4", "karma": "^6.3.16", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^2.2.0", diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index f63fe20fdead..f0e167db50b4 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -60,6 +60,7 @@ export { setTags, setUser, withScope, + withIsolationScope, FunctionToString, InboundFilters, metrics, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index d9afa470b2ba..97abefea8242 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -46,6 +46,7 @@ export { setMeasurement, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, spanStatusfromHttpCode, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 831059e97756..9799e6e73b4f 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -68,6 +68,8 @@ const breadcrumbsIntegration: IntegrationFn = (options: Partial { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(currentEvent) { // We want to ignore any non-error type events, e.g. transactions or replays // These should never be deduped, and also not be compared against as _previousEvent. diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts index 2347c7cb1971..d46f29257306 100644 --- a/packages/browser/src/integrations/httpcontext.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -8,6 +8,8 @@ const INTEGRATION_NAME = 'HttpContext'; const httpContextIntegration: IntegrationFn = () => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function preprocessEvent(event) { // if none of the information we want exists, don't bother if (!WINDOW.navigator && !WINDOW.location && !WINDOW.document) { diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index e74e6252a4ce..bb351bcdc3c6 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -19,6 +19,8 @@ const linkedErrorsIntegration: IntegrationFn = (options: LinkedErrorsOptions = { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function preprocessEvent(event, hint, client) { const options = client.getOptions(); diff --git a/packages/browser/src/profiling/hubextensions.ts b/packages/browser/src/profiling/hubextensions.ts index 462929fff04b..9fd156a1b90b 100644 --- a/packages/browser/src/profiling/hubextensions.ts +++ b/packages/browser/src/profiling/hubextensions.ts @@ -1,4 +1,5 @@ /* eslint-disable complexity */ +import { spanToJSON } from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { logger, timestampInSeconds, uuid4 } from '@sentry/utils'; @@ -56,7 +57,7 @@ export function startProfileForTransaction(transaction: Transaction): Transactio } if (DEBUG_BUILD) { - logger.log(`[Profiling] started profiling transaction: ${transaction.name || transaction.description}`); + logger.log(`[Profiling] started profiling transaction: ${spanToJSON(transaction).description}`); } // We create "unique" transaction names to avoid concurrent transactions with same names @@ -87,11 +88,7 @@ export function startProfileForTransaction(transaction: Transaction): Transactio } if (processedProfile) { if (DEBUG_BUILD) { - logger.log( - '[Profiling] profile for:', - transaction.name || transaction.description, - 'already exists, returning early', - ); + logger.log('[Profiling] profile for:', spanToJSON(transaction).description, 'already exists, returning early'); } return null; } @@ -105,14 +102,14 @@ export function startProfileForTransaction(transaction: Transaction): Transactio } if (DEBUG_BUILD) { - logger.log(`[Profiling] stopped profiling of transaction: ${transaction.name || transaction.description}`); + logger.log(`[Profiling] stopped profiling of transaction: ${spanToJSON(transaction).description}`); } // In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile. if (!profile) { if (DEBUG_BUILD) { logger.log( - `[Profiling] profiler returned null profile for: ${transaction.name || transaction.description}`, + `[Profiling] profiler returned null profile for: ${spanToJSON(transaction).description}`, 'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started', ); } @@ -135,7 +132,7 @@ export function startProfileForTransaction(transaction: Transaction): Transactio if (DEBUG_BUILD) { logger.log( '[Profiling] max profile duration elapsed, stopping profiling for:', - transaction.name || transaction.description, + spanToJSON(transaction).description, ); } // If the timeout exceeds, we want to stop profiling, but not finish the transaction @@ -159,6 +156,8 @@ export function startProfileForTransaction(transaction: Transaction): Transactio // Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared. void onProfileHandler().then( () => { + // TODO: Can we rewrite this to use attributes? + // eslint-disable-next-line deprecation/deprecation transaction.setContext('profile', { profile_id: profileId, start_timestamp: startTimestamp }); originalEnd(); }, diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 3f823429c122..c6e478e8554d 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -21,9 +21,12 @@ const INTEGRATION_NAME = 'BrowserProfiling'; const browserProfilingIntegration: IntegrationFn = () => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client) { const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); if (transaction && isAutomatedPageLoadTransaction(transaction)) { diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 579bb57e9698..136700432dbb 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,11 +1,13 @@ import type { Hub } from '@sentry/core'; import { Integrations as CoreIntegrations, + captureSession, getClient, getCurrentHub, getIntegrationsToSetup, getReportDialogEndpoint, initAndBind, + startSession, } from '@sentry/core'; import type { UserFeedback } from '@sentry/types'; import { @@ -165,6 +167,7 @@ export const showReportDialog: ShowReportDialogFunction = ( return; } + // eslint-disable-next-line deprecation/deprecation const { client, scope } = hub.getStackTop(); const dsn = options.dsn || (client && client.getDsn()); if (!dsn) { @@ -250,11 +253,6 @@ export function wrap(fn: (...args: any) => any): any { return internalWrap(fn)(); } -function startSessionOnHub(hub: Hub): void { - hub.startSession({ ignoreDuration: true }); - hub.captureSession(); -} - /** * Enable automatic Session Tracking for the initial page load. */ @@ -264,29 +262,19 @@ function startSessionTracking(): void { return; } - const hub = getCurrentHub(); - - // The only way for this to be false is for there to be a version mismatch between @sentry/browser (>= 6.0.0) and - // @sentry/hub (< 5.27.0). In the simple case, there won't ever be such a mismatch, because the two packages are - // pinned at the same version in package.json, but there are edge cases where it's possible. See - // https://github.com/getsentry/sentry-javascript/issues/3207 and - // https://github.com/getsentry/sentry-javascript/issues/3234 and - // https://github.com/getsentry/sentry-javascript/issues/3278. - if (!hub.captureSession) { - return; - } - // The session duration for browser sessions does not track a meaningful // concept that can be used as a metric. // Automatically captured sessions are akin to page views, and thus we // discard their duration. - startSessionOnHub(hub); + startSession({ ignoreDuration: true }); + captureSession(); // We want to create a session for every navigation as well addHistoryInstrumentationHandler(({ from, to }) => { // Don't create an additional session for the initial route or if the location did not change if (from !== undefined && from !== to) { - startSessionOnHub(getCurrentHub()); + startSession({ ignoreDuration: true }); + captureSession(); } }); } diff --git a/packages/browser/test/unit/profiling/hubextensions.test.ts b/packages/browser/test/unit/profiling/hubextensions.test.ts index d0f9d488ec91..30b7769e836d 100644 --- a/packages/browser/test/unit/profiling/hubextensions.test.ts +++ b/packages/browser/test/unit/profiling/hubextensions.test.ts @@ -10,6 +10,9 @@ import { JSDOM } from 'jsdom'; import { onProfilingStartRouteTransaction } from '../../../src'; +// eslint-disable-next-line no-bitwise +const TraceFlagSampled = 0x1 << 0; + // @ts-expect-error store a reference so we can reset it later const globalDocument = global.document; // @ts-expect-error store a reference so we can reset it later @@ -67,9 +70,17 @@ describe('BrowserProfilingIntegration', () => { // @ts-expect-error force api to be undefined global.window.Profiler = undefined; // set sampled to true so that profiling does not early return - const mockTransaction = { isRecording: () => true } as Transaction; + const mockTransaction = { + isRecording: () => true, + spanContext: () => ({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TraceFlagSampled, + }), + } as Transaction; expect(() => onProfilingStartRouteTransaction(mockTransaction)).not.toThrow(); }); + it('does not throw if constructor throws', () => { const spy = jest.fn(); @@ -80,8 +91,15 @@ describe('BrowserProfilingIntegration', () => { } } - // set isRecording to true so that profiling does not early return - const mockTransaction = { isRecording: () => true } as Transaction; + // set sampled to true so that profiling does not early return + const mockTransaction = { + isRecording: () => true, + spanContext: () => ({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TraceFlagSampled, + }), + } as Transaction; // @ts-expect-error override with our own constructor global.window.Profiler = Profiler; diff --git a/packages/browser/test/unit/profiling/integration.test.ts b/packages/browser/test/unit/profiling/integration.test.ts index ae95927ac2cd..0c7eb35f60e2 100644 --- a/packages/browser/test/unit/profiling/integration.test.ts +++ b/packages/browser/test/unit/profiling/integration.test.ts @@ -50,7 +50,8 @@ describe('BrowserProfilingIntegration', () => { const client = Sentry.getClient(); - const currentTransaction = Sentry.getCurrentHub().getScope().getTransaction(); + // eslint-disable-next-line deprecation/deprecation + const currentTransaction = Sentry.getCurrentScope().getTransaction(); expect(currentTransaction?.op).toBe('pageload'); currentTransaction?.end(); await client?.flush(1000); diff --git a/packages/browser/test/unit/sdk.test.ts b/packages/browser/test/unit/sdk.test.ts index fe977707920b..75cb238d35bb 100644 --- a/packages/browser/test/unit/sdk.test.ts +++ b/packages/browser/test/unit/sdk.test.ts @@ -42,7 +42,7 @@ jest.mock('@sentry/core', () => { return new Scope(); }, bindClient(client: Client): boolean { - client.setupIntegrations(); + client.init!(); return true; }, }; diff --git a/packages/bun/package.json b/packages/bun/package.json index 2ecb225a5fbb..82953126129b 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for bun", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bun", @@ -29,10 +29,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.92.0", - "@sentry/node": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/core": "7.93.0", + "@sentry/node": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "devDependencies": { "bun-types": "latest" diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 749299badd74..83b4b90ab01b 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -39,6 +39,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -65,6 +66,7 @@ export { // eslint-disable-next-line deprecation/deprecation trace, withScope, + withIsolationScope, captureCheckIn, withMonitor, setMeasurement, diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 89d245908400..fec3aae439af 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -1,8 +1,10 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction, captureException, continueTrace, convertIntegrationFnToClass, + getCurrentScope, runWithAsyncContext, startSpan, } from '@sentry/core'; @@ -54,6 +56,7 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] const parsedUrl = parseUrl(request.url); const data: Record = { 'http.request.method': request.method || 'GET', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }; if (parsedUrl.search) { data['http.query'] = parsedUrl.search; @@ -72,8 +75,8 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] ...ctx, data, metadata: { + // eslint-disable-next-line deprecation/deprecation ...ctx.metadata, - source: 'url', request: { url, method: request.method, @@ -88,9 +91,10 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] >); if (response && response.status) { span?.setHttpStatus(response.status); - span?.setData('http.response.status_code', response.status); + span?.setAttribute('http.response.status_code', response.status); if (span instanceof Transaction) { - span.setContext('response', { + const scope = getCurrentScope(); + scope.setContext('response', { headers: response.headers.toJSON(), status_code: response.status, }); diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index 4fe00180377c..286d30749dc1 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -1,5 +1,5 @@ import { beforeAll, beforeEach, describe, expect, test } from 'bun:test'; -import { Hub, makeMain } from '@sentry/core'; +import { Hub, getDynamicSamplingContextFromSpan, makeMain, spanIsSampled, spanToJSON } from '@sentry/core'; import { BunClient } from '../../src/client'; import { instrumentBunServe } from '../../src/integrations/bunserver'; @@ -26,11 +26,12 @@ describe('Bun Serve Integration', () => { test('generates a transaction around a request', async () => { client.on('finishTransaction', transaction => { expect(transaction.status).toBe('ok'); + // eslint-disable-next-line deprecation/deprecation expect(transaction.tags).toEqual({ 'http.status_code': '200', }); expect(transaction.op).toEqual('http.server'); - expect(transaction.name).toEqual('GET /'); + expect(spanToJSON(transaction).description).toEqual('GET /'); }); const server = Bun.serve({ @@ -48,11 +49,12 @@ describe('Bun Serve Integration', () => { test('generates a post transaction', async () => { client.on('finishTransaction', transaction => { expect(transaction.status).toBe('ok'); + // eslint-disable-next-line deprecation/deprecation expect(transaction.tags).toEqual({ 'http.status_code': '200', }); expect(transaction.op).toEqual('http.server'); - expect(transaction.name).toEqual('POST /'); + expect(spanToJSON(transaction).description).toEqual('POST /'); }); const server = Bun.serve({ @@ -78,11 +80,18 @@ describe('Bun Serve Integration', () => { const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-environment=production'; client.on('finishTransaction', transaction => { - expect(transaction.traceId).toBe(TRACE_ID); + expect(transaction.spanContext().traceId).toBe(TRACE_ID); expect(transaction.parentSpanId).toBe(PARENT_SPAN_ID); - expect(transaction.isRecording()).toBe(true); + expect(spanIsSampled(transaction)).toBe(true); + // span.endTimestamp is already set in `finishTransaction` hook + expect(transaction.isRecording()).toBe(false); + // eslint-disable-next-line deprecation/deprecation expect(transaction.metadata?.dynamicSamplingContext).toStrictEqual({ version: '1.0', environment: 'production' }); + expect(getDynamicSamplingContextFromSpan(transaction)).toStrictEqual({ + version: '1.0', + environment: 'production', + }); }); const server = Bun.serve({ diff --git a/packages/core/package.json b/packages/core/package.json index 3ef693023f85..05fda3d9cbca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "7.92.0", + "version": "7.93.0", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", @@ -29,8 +29,8 @@ "access": "public" }, "dependencies": { - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "scripts": { "build": "run-p build:transpile build:types", @@ -56,5 +56,12 @@ "volta": { "extends": "../../package.json" }, + "madge": { + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } + }, "sideEffects": false } diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 628df591248d..d59d596e0b82 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -314,12 +314,19 @@ export abstract class BaseClient implements Client { } /** - * Sets up the integrations + * This is an internal function to setup all integrations that should run on the client. + * @deprecated Use `client.init()` instead. */ public setupIntegrations(forceInitialize?: boolean): void { if ((forceInitialize && !this._integrationsInitialized) || (this._isEnabled() && !this._integrationsInitialized)) { - this._integrations = setupIntegrations(this, this._options.integrations); - this._integrationsInitialized = true; + this._setupIntegrations(); + } + } + + /** @inheritdoc */ + public init(): void { + if (this._isEnabled()) { + this._setupIntegrations(); } } @@ -327,13 +334,24 @@ export abstract class BaseClient implements Client { * Gets an installed integration by its `id`. * * @returns The installed integration or `undefined` if no integration with that `id` was installed. + * @deprecated Use `getIntegrationByName()` instead. */ public getIntegrationById(integrationId: string): Integration | undefined { - return this._integrations[integrationId]; + return this.getIntegrationByName(integrationId); } /** - * @inheritDoc + * Gets an installed integration by its name. + * + * @returns The installed integration or `undefined` if no integration with that `name` was installed. + */ + public getIntegrationByName(integrationName: string): T | undefined { + return this._integrations[integrationName] as T | undefined; + } + + /** + * Returns the client's instance of the given integration class, it any. + * @deprecated Use `getIntegrationByName()` instead. */ public getIntegration(integration: IntegrationClass): T | null { try { @@ -512,6 +530,13 @@ export abstract class BaseClient implements Client { /* eslint-enable @typescript-eslint/unified-signatures */ + /** Setup integrations for this client. */ + protected _setupIntegrations(): void { + this._integrations = setupIntegrations(this, this._options.integrations); + // TODO v8: We don't need this flag anymore + this._integrationsInitialized = true; + } + /** Updates existing session based on the provided event */ protected _updateSessionFromEvent(session: Session, event: Event): void { let crashed = false; diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 09989215ef3e..2d84275c08c1 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -12,66 +12,71 @@ import type { FinishedCheckIn, MonitorConfig, Primitive, + Scope as ScopeInterface, + Session, + SessionContext, Severity, SeverityLevel, TransactionContext, User, } from '@sentry/types'; -import { isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; +import { GLOBAL_OBJ, isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; +import { DEFAULT_ENVIRONMENT } from './constants'; import { DEBUG_BUILD } from './debug-build'; import type { Hub } from './hub'; -import { getCurrentHub } from './hub'; +import { runWithAsyncContext } from './hub'; +import { getCurrentHub, getIsolationScope } from './hub'; import type { Scope } from './scope'; +import { closeSession, makeSession, updateSession } from './session'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; -// Note: All functions in this file are typed with a return value of `ReturnType`, -// where HUB_FUNCTION is some method on the Hub class. -// -// This is done to make sure the top level SDK methods stay in sync with the hub methods. -// Although every method here has an explicit return type, some of them (that map to void returns) do not -// contain `return` keywords. This is done to save on bundle size, as `return` is not minifiable. - /** * Captures an exception event and sends it to Sentry. - * This accepts an event hint as optional second parameter. - * Alternatively, you can also pass a CaptureContext directly as second parameter. + * + * @param exception The exception to capture. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. */ export function captureException( // eslint-disable-next-line @typescript-eslint/no-explicit-any exception: any, hint?: ExclusiveEventHintOrCaptureContext, -): ReturnType { +): string { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().captureException(exception, parseEventHintOrCaptureContext(hint)); } /** * Captures a message event and sends it to Sentry. * - * @param message The message to send to Sentry. - * @param Severity Define the level of the message. - * @returns The generated eventId. + * @param exception The exception to capture. + * @param captureContext Define the level of the message or pass in additional data to attach to the message. + * @returns the id of the captured message. */ export function captureMessage( message: string, // eslint-disable-next-line deprecation/deprecation captureContext?: CaptureContext | Severity | SeverityLevel, -): ReturnType { +): string { // This is necessary to provide explicit scopes upgrade, without changing the original // arity of the `captureMessage(message, level)` method. const level = typeof captureContext === 'string' ? captureContext : undefined; const context = typeof captureContext !== 'string' ? { captureContext } : undefined; + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().captureMessage(message, level, context); } /** * Captures a manually created event and sends it to Sentry. * - * @param event The event to send to Sentry. - * @returns The generated eventId. + * @param exception The event to send to Sentry. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured event. */ -export function captureEvent(event: Event, hint?: EventHint): ReturnType { +export function captureEvent(event: Event, hint?: EventHint): string { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().captureEvent(event, hint); } @@ -95,6 +100,7 @@ export function configureScope(callback: (scope: Scope) => void): ReturnType { + // eslint-disable-next-line deprecation/deprecation getCurrentHub().addBreadcrumb(breadcrumb, hint); } @@ -105,6 +111,7 @@ export function addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): Re */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function setContext(name: string, context: { [key: string]: any } | null): ReturnType { + // eslint-disable-next-line deprecation/deprecation getCurrentHub().setContext(name, context); } @@ -113,6 +120,7 @@ export function setContext(name: string, context: { [key: string]: any } | null) * @param extras Extras object to merge into current context. */ export function setExtras(extras: Extras): ReturnType { + // eslint-disable-next-line deprecation/deprecation getCurrentHub().setExtras(extras); } @@ -122,6 +130,7 @@ export function setExtras(extras: Extras): ReturnType { * @param extra Any kind of data. This data will be normalized. */ export function setExtra(key: string, extra: Extra): ReturnType { + // eslint-disable-next-line deprecation/deprecation getCurrentHub().setExtra(key, extra); } @@ -130,6 +139,7 @@ export function setExtra(key: string, extra: Extra): ReturnType * @param tags Tags context object to merge into current context. */ export function setTags(tags: { [key: string]: Primitive }): ReturnType { + // eslint-disable-next-line deprecation/deprecation getCurrentHub().setTags(tags); } @@ -142,6 +152,7 @@ export function setTags(tags: { [key: string]: Primitive }): ReturnType { + // eslint-disable-next-line deprecation/deprecation getCurrentHub().setTag(key, value); } @@ -151,6 +162,7 @@ export function setTag(key: string, value: Primitive): ReturnType * @param user User context object to be set in the current context. Pass `null` to unset the user. */ export function setUser(user: User | null): ReturnType { + // eslint-disable-next-line deprecation/deprecation getCurrentHub().setUser(user); } @@ -164,11 +176,57 @@ export function setUser(user: User | null): ReturnType { * pushScope(); * callback(); * popScope(); + */ +export function withScope(callback: (scope: Scope) => T): T; +/** + * Set the given scope as the active scope in the callback. + */ +export function withScope(scope: ScopeInterface | undefined, callback: (scope: Scope) => T): T; +/** + * Either creates a new active scope, or sets the given scope as active scope in the given callback. + */ +export function withScope( + ...rest: [callback: (scope: Scope) => T] | [scope: ScopeInterface | undefined, callback: (scope: Scope) => T] +): T { + // If a scope is defined, we want to make this the active scope instead of the default one + if (rest.length === 2) { + const [scope, callback] = rest; + if (!scope) { + // eslint-disable-next-line deprecation/deprecation + return getCurrentHub().withScope(callback); + } + + const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + return hub.withScope(() => { + // eslint-disable-next-line deprecation/deprecation + hub.getStackTop().scope = scope as Scope; + return callback(scope as Scope); + }); + } + + // eslint-disable-next-line deprecation/deprecation + return getCurrentHub().withScope(rest[0]); +} + +/** + * Attempts to fork the current isolation scope and the current scope based on the current async context strategy. If no + * async context strategy is set, the isolation scope and the current scope will not be forked (this is currently the + * case, for example, in the browser). + * + * Usage of this function in environments without async context strategy is discouraged and may lead to unexpected behaviour. * - * @param callback that will be enclosed into push/popScope. + * This function is intended for Sentry SDK and SDK integration development. It is not recommended to be used in "normal" + * applications directly because it comes with pitfalls. Use at your own risk! + * + * @param callback The callback in which the passed isolation scope is active. (Note: In environments without async + * context strategy, the currently active isolation scope may change within execution of the callback.) + * @returns The same value that `callback` returns. */ -export function withScope(callback: (scope: Scope) => T): T { - return getCurrentHub().withScope(callback); +export function withIsolationScope(callback: (isolationScope: Scope) => T): T { + return runWithAsyncContext(() => { + return callback(getIsolationScope()); + }); } /** @@ -306,6 +364,7 @@ export async function close(timeout?: number): Promise { * @deprecated This function will be removed in the next major version of the Sentry SDK. */ export function lastEventId(): string | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().lastEventId(); } @@ -313,6 +372,7 @@ export function lastEventId(): string | undefined { * Get the currently active client. */ export function getClient(): C | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().getClient(); } @@ -320,5 +380,102 @@ export function getClient(): C | undefined { * Get the currently active scope. */ export function getCurrentScope(): Scope { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().getScope(); } + +/** + * Start a session on the current isolation scope. + * + * @param context (optional) additional properties to be applied to the returned session object + * + * @returns the new active session + */ +export function startSession(context?: SessionContext): Session { + const client = getClient(); + const isolationScope = getIsolationScope(); + const currentScope = getCurrentScope(); + + const { release, environment = DEFAULT_ENVIRONMENT } = (client && client.getOptions()) || {}; + + // Will fetch userAgent if called from browser sdk + const { userAgent } = GLOBAL_OBJ.navigator || {}; + + const session = makeSession({ + release, + environment, + user: currentScope.getUser() || isolationScope.getUser(), + ...(userAgent && { userAgent }), + ...context, + }); + + // End existing session if there's one + const currentSession = isolationScope.getSession(); + if (currentSession && currentSession.status === 'ok') { + updateSession(currentSession, { status: 'exited' }); + } + + endSession(); + + // Afterwards we set the new session on the scope + isolationScope.setSession(session); + + // TODO (v8): Remove this and only use the isolation scope(?). + // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() + currentScope.setSession(session); + + return session; +} + +/** + * End the session on the current isolation scope. + */ +export function endSession(): void { + const isolationScope = getIsolationScope(); + const currentScope = getCurrentScope(); + + const session = currentScope.getSession() || isolationScope.getSession(); + if (session) { + closeSession(session); + } + _sendSessionUpdate(); + + // the session is over; take it off of the scope + isolationScope.setSession(); + + // TODO (v8): Remove this and only use the isolation scope(?). + // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() + currentScope.setSession(); +} + +/** + * Sends the current Session on the scope + */ +function _sendSessionUpdate(): void { + const isolationScope = getIsolationScope(); + const currentScope = getCurrentScope(); + const client = getClient(); + // TODO (v8): Remove currentScope and only use the isolation scope(?). + // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() + const session = currentScope.getSession() || isolationScope.getSession(); + if (session && client && client.captureSession) { + client.captureSession(session); + } +} + +/** + * Sends the current session on the scope to Sentry + * + * @param end If set the session will be marked as exited and removed from the scope. + * Defaults to `false`. + */ +export function captureSession(end: boolean = false): void { + // both send the update and pull the session from the scope + if (end) { + endSession(); + return; + } + + // only send the update + _sendSessionUpdate(); +} diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 1db3b01e7b0e..815a55293516 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -154,7 +154,12 @@ export class Hub implements HubInterface { } /** - * @inheritDoc + * Checks if this hub's version is older than the given version. + * + * @param version A version number to compare to. + * @return True if the given version is newer; otherwise false. + * + * @deprecated This will be removed in v8. */ public isOlderThan(version: number): boolean { return this._version < version; @@ -164,9 +169,13 @@ export class Hub implements HubInterface { * @inheritDoc */ public bindClient(client?: Client): void { + // eslint-disable-next-line deprecation/deprecation const top = this.getStackTop(); top.client = client; + top.scope.setClient(client); + // eslint-disable-next-line deprecation/deprecation if (client && client.setupIntegrations) { + // eslint-disable-next-line deprecation/deprecation client.setupIntegrations(); } } @@ -178,8 +187,11 @@ export class Hub implements HubInterface { */ public pushScope(): Scope { // We want to clone the content of prev scope + // eslint-disable-next-line deprecation/deprecation const scope = this.getScope().clone(); + // eslint-disable-next-line deprecation/deprecation this.getStack().push({ + // eslint-disable-next-line deprecation/deprecation client: this.getClient(), scope, }); @@ -192,12 +204,16 @@ export class Hub implements HubInterface { * @deprecated Use `withScope` instead. */ public popScope(): boolean { + // eslint-disable-next-line deprecation/deprecation if (this.getStack().length <= 1) return false; + // eslint-disable-next-line deprecation/deprecation return !!this.getStack().pop(); } /** * @inheritDoc + * + * @deprecated Use `Sentry.withScope()` instead. */ public withScope(callback: (scope: Scope) => T): T { // eslint-disable-next-line deprecation/deprecation @@ -235,54 +251,70 @@ export class Hub implements HubInterface { /** * @inheritDoc + * + * @deprecated Use `Sentry.getClient()` instead. */ public getClient(): C | undefined { + // eslint-disable-next-line deprecation/deprecation return this.getStackTop().client as C; } - /** Returns the scope of the top stack. */ + /** + * Returns the scope of the top stack. + * + * @deprecated Use `Sentry.getCurrentScope()` instead. + */ public getScope(): Scope { + // eslint-disable-next-line deprecation/deprecation return this.getStackTop().scope; } - /** @inheritdoc */ + /** + * @deprecated Use `Sentry.getIsolationScope()` instead. + */ public getIsolationScope(): Scope { return this._isolationScope; } - /** Returns the scope stack for domains or the process. */ + /** + * Returns the scope stack for domains or the process. + * @deprecated This will be removed in v8. + */ public getStack(): Layer[] { return this._stack; } - /** Returns the topmost scope layer in the order domain > local > process. */ + /** + * Returns the topmost scope layer in the order domain > local > process. + * @deprecated This will be removed in v8. + */ public getStackTop(): Layer { return this._stack[this._stack.length - 1]; } /** * @inheritDoc + * + * @deprecated Use `Sentry.captureException()` instead. */ public captureException(exception: unknown, hint?: EventHint): string { const eventId = (this._lastEventId = hint && hint.event_id ? hint.event_id : uuid4()); const syntheticException = new Error('Sentry syntheticException'); - this._withClient((client, scope) => { - client.captureException( - exception, - { - originalException: exception, - syntheticException, - ...hint, - event_id: eventId, - }, - scope, - ); + // eslint-disable-next-line deprecation/deprecation + this.getScope().captureException(exception, { + originalException: exception, + syntheticException, + ...hint, + event_id: eventId, }); + return eventId; } /** * @inheritDoc + * + * @deprecated Use `Sentry.captureMessage()` instead. */ public captureMessage( message: string, @@ -292,39 +324,36 @@ export class Hub implements HubInterface { ): string { const eventId = (this._lastEventId = hint && hint.event_id ? hint.event_id : uuid4()); const syntheticException = new Error(message); - this._withClient((client, scope) => { - client.captureMessage( - message, - level, - { - originalException: message, - syntheticException, - ...hint, - event_id: eventId, - }, - scope, - ); + // eslint-disable-next-line deprecation/deprecation + this.getScope().captureMessage(message, level, { + originalException: message, + syntheticException, + ...hint, + event_id: eventId, }); + return eventId; } /** * @inheritDoc + * + * @deprecated Use `Sentry.captureEvent()` instead. */ public captureEvent(event: Event, hint?: EventHint): string { const eventId = hint && hint.event_id ? hint.event_id : uuid4(); if (!event.type) { this._lastEventId = eventId; } - - this._withClient((client, scope) => { - client.captureEvent(event, { ...hint, event_id: eventId }, scope); - }); + // eslint-disable-next-line deprecation/deprecation + this.getScope().captureEvent(event, { ...hint, event_id: eventId }); return eventId; } /** * @inheritDoc + * + * @deprecated This will be removed in v8. */ public lastEventId(): string | undefined { return this._lastEventId; @@ -332,8 +361,11 @@ export class Hub implements HubInterface { /** * @inheritDoc + * + * @deprecated Use `Sentry.addBreadcrumb()` instead. */ public addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void { + // eslint-disable-next-line deprecation/deprecation const { scope, client } = this.getStackTop(); if (!client) return; @@ -355,50 +387,88 @@ export class Hub implements HubInterface { client.emit('beforeAddBreadcrumb', finalBreadcrumb, hint); } + // TODO(v8): I know this comment doesn't make much sense because the hub will be deprecated but I still wanted to + // write it down. In theory, we would have to add the breadcrumbs to the isolation scope here, however, that would + // duplicate all of the breadcrumbs. There was the possibility of adding breadcrumbs to both, the isolation scope + // and the normal scope, and deduplicating it down the line in the event processing pipeline. However, that would + // have been very fragile, because the breadcrumb objects would have needed to keep their identity all throughout + // the event processing pipeline. + // In the new implementation, the top level `Sentry.addBreadcrumb()` should ONLY write to the isolation scope. + scope.addBreadcrumb(finalBreadcrumb, maxBreadcrumbs); } /** * @inheritDoc + * @deprecated Use `Sentry.setUser()` instead. */ public setUser(user: User | null): void { + // TODO(v8): The top level `Sentry.setUser()` function should write ONLY to the isolation scope. + // eslint-disable-next-line deprecation/deprecation this.getScope().setUser(user); + // eslint-disable-next-line deprecation/deprecation + this.getIsolationScope().setUser(user); } /** * @inheritDoc + * @deprecated Use `Sentry.setTags()` instead. */ public setTags(tags: { [key: string]: Primitive }): void { + // TODO(v8): The top level `Sentry.setTags()` function should write ONLY to the isolation scope. + // eslint-disable-next-line deprecation/deprecation this.getScope().setTags(tags); + // eslint-disable-next-line deprecation/deprecation + this.getIsolationScope().setTags(tags); } /** * @inheritDoc + * @deprecated Use `Sentry.setExtras()` instead. */ public setExtras(extras: Extras): void { + // TODO(v8): The top level `Sentry.setExtras()` function should write ONLY to the isolation scope. + // eslint-disable-next-line deprecation/deprecation this.getScope().setExtras(extras); + // eslint-disable-next-line deprecation/deprecation + this.getIsolationScope().setExtras(extras); } /** * @inheritDoc + * @deprecated Use `Sentry.setTag()` instead. */ public setTag(key: string, value: Primitive): void { + // TODO(v8): The top level `Sentry.setTag()` function should write ONLY to the isolation scope. + // eslint-disable-next-line deprecation/deprecation this.getScope().setTag(key, value); + // eslint-disable-next-line deprecation/deprecation + this.getIsolationScope().setTag(key, value); } /** * @inheritDoc + * @deprecated Use `Sentry.setExtra()` instead. */ public setExtra(key: string, extra: Extra): void { + // TODO(v8): The top level `Sentry.setExtra()` function should write ONLY to the isolation scope. + // eslint-disable-next-line deprecation/deprecation this.getScope().setExtra(key, extra); + // eslint-disable-next-line deprecation/deprecation + this.getIsolationScope().setExtra(key, extra); } /** * @inheritDoc + * @deprecated Use `Sentry.setContext()` instead. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any public setContext(name: string, context: { [key: string]: any } | null): void { + // TODO(v8): The top level `Sentry.setContext()` function should write ONLY to the isolation scope. + // eslint-disable-next-line deprecation/deprecation this.getScope().setContext(name, context); + // eslint-disable-next-line deprecation/deprecation + this.getIsolationScope().setContext(name, context); } /** @@ -407,6 +477,7 @@ export class Hub implements HubInterface { * @deprecated Use `getScope()` directly. */ public configureScope(callback: (scope: Scope) => void): void { + // eslint-disable-next-line deprecation/deprecation const { scope, client } = this.getStackTop(); if (client) { callback(scope); @@ -427,11 +498,14 @@ export class Hub implements HubInterface { /** * @inheritDoc + * @deprecated Use `Sentry.getClient().getIntegrationByName()` instead. */ public getIntegration(integration: IntegrationClass): T | null { + // eslint-disable-next-line deprecation/deprecation const client = this.getClient(); if (!client) return null; try { + // eslint-disable-next-line deprecation/deprecation return client.getIntegration(integration); } catch (_oO) { DEBUG_BUILD && logger.warn(`Cannot retrieve integration ${integration.id} from the current Hub`); @@ -462,6 +536,7 @@ export class Hub implements HubInterface { const result = this._callExtensionMethod('startTransaction', context, customSamplingContext); if (DEBUG_BUILD && !result) { + // eslint-disable-next-line deprecation/deprecation const client = this.getClient(); if (!client) { logger.warn( @@ -480,6 +555,7 @@ Sentry.init({...}); /** * @inheritDoc + * @deprecated Use `spanToTraceHeader()` instead. */ public traceHeaders(): { [key: string]: string } { return this._callExtensionMethod<{ [key: string]: string }>('traceHeaders'); @@ -487,10 +563,13 @@ Sentry.init({...}); /** * @inheritDoc + * + * @deprecated Use top level `captureSession` instead. */ public captureSession(endSession: boolean = false): void { // both send the update and pull the session from the scope if (endSession) { + // eslint-disable-next-line deprecation/deprecation return this.endSession(); } @@ -500,8 +579,10 @@ Sentry.init({...}); /** * @inheritDoc + * @deprecated Use top level `endSession` instead. */ public endSession(): void { + // eslint-disable-next-line deprecation/deprecation const layer = this.getStackTop(); const scope = layer.scope; const session = scope.getSession(); @@ -516,8 +597,10 @@ Sentry.init({...}); /** * @inheritDoc + * @deprecated Use top level `startSession` instead. */ public startSession(context?: SessionContext): Session { + // eslint-disable-next-line deprecation/deprecation const { scope, client } = this.getStackTop(); const { release, environment = DEFAULT_ENVIRONMENT } = (client && client.getOptions()) || {}; @@ -537,6 +620,7 @@ Sentry.init({...}); if (currentSession && currentSession.status === 'ok') { updateSession(currentSession, { status: 'exited' }); } + // eslint-disable-next-line deprecation/deprecation this.endSession(); // Afterwards we set the new session on the scope @@ -553,6 +637,7 @@ Sentry.init({...}); * only unnecessarily increased API surface but only wrapped accessing the option. */ public shouldSendDefaultPii(): boolean { + // eslint-disable-next-line deprecation/deprecation const client = this.getClient(); const options = client && client.getOptions(); return Boolean(options && options.sendDefaultPii); @@ -562,6 +647,7 @@ Sentry.init({...}); * Sends the current Session on the scope */ private _sendSessionUpdate(): void { + // eslint-disable-next-line deprecation/deprecation const { scope, client } = this.getStackTop(); const session = scope.getSession(); @@ -570,19 +656,6 @@ Sentry.init({...}); } } - /** - * Internal helper function to call a method on the top client if it exists. - * - * @param method The method to call on the client. - * @param args Arguments to pass to the client function. - */ - private _withClient(callback: (client: Client, scope: Scope) => void): void { - const { scope, client } = this.getStackTop(); - if (client) { - callback(client, scope); - } - } - /** * Calls global extension method and binding current instance to the function call */ @@ -654,12 +727,18 @@ export function getCurrentHub(): Hub { * meaning that it will remain stable for the same Hub. */ export function getIsolationScope(): Scope { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().getIsolationScope(); } function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { // If there's no hub, or its an old API, assign a new one - if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { + + if ( + !hasHubOnCarrier(registry) || + // eslint-disable-next-line deprecation/deprecation + getHubFromCarrier(registry).isOlderThan(API_VERSION) + ) { setHubOnCarrier(registry, new Hub()); } @@ -674,9 +753,16 @@ function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { */ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub()): void { // If there's no hub on current domain, or it's an old API, assign a new one - if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { + if ( + !hasHubOnCarrier(carrier) || + // eslint-disable-next-line deprecation/deprecation + getHubFromCarrier(carrier).isOlderThan(API_VERSION) + ) { + // eslint-disable-next-line deprecation/deprecation const client = parent.getClient(); + // eslint-disable-next-line deprecation/deprecation const scope = parent.getScope(); + // eslint-disable-next-line deprecation/deprecation const isolationScope = parent.getIsolationScope(); setHubOnCarrier(carrier, new Hub(client, scope.clone(), isolationScope.clone())); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2c423f0744c3..026f9b5164d9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,6 +5,7 @@ export type { ServerRuntimeClientOptions } from './server-runtime-client'; export type { RequestDataIntegrationOptions } from './integrations/requestdata'; export * from './tracing'; +export * from './semanticAttributes'; export { createEventEnvelope, createSessionEnvelope } from './envelope'; export { addBreadcrumb, @@ -28,8 +29,12 @@ export { setTags, setUser, withScope, + withIsolationScope, getClient, getCurrentScope, + startSession, + endSession, + captureSession, } from './exports'; export { getCurrentHub, @@ -72,7 +77,12 @@ export { createCheckInEnvelope } from './checkin'; export { hasTracingEnabled } from './utils/hasTracingEnabled'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { handleCallbackErrors } from './utils/handleCallbackErrors'; -export { spanToTraceHeader } from './utils/spanUtils'; +export { + spanToTraceHeader, + spanToJSON, + spanIsSampled, +} from './utils/spanUtils'; +export { getRootSpan } from './utils/getRootSpan'; export { DEFAULT_ENVIRONMENT } from './constants'; export { ModuleMetadata } from './integrations/metadata'; export { RequestData } from './integrations/requestdata'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index f9be8b325782..4b4a3177c428 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,4 +1,4 @@ -import type { Client, Event, EventHint, EventProcessor, Hub, Integration, IntegrationFn, Options } from '@sentry/types'; +import type { Client, Event, EventHint, Integration, IntegrationFn, Options } from '@sentry/types'; import { arrayify, logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; @@ -101,6 +101,10 @@ export function setupIntegrations(client: Client, integrations: Integration[]): /** Setup a single integration. */ export function setupIntegration(client: Client, integration: Integration, integrationIndex: IntegrationIndex): void { + if (integrationIndex[integration.name]) { + DEBUG_BUILD && logger.log(`Integration skipped because it was already installed: ${integration.name}`); + return; + } integrationIndex[integration.name] = integration; // `setupOnce` is only called the first time @@ -167,26 +171,16 @@ export function convertIntegrationFnToClass( fn: Fn, ): Integration & { id: string; - new (...args: Parameters): Integration & - ReturnType & { - setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; - }; + new (...args: Parameters): Integration & ReturnType; } { return Object.assign( // eslint-disable-next-line @typescript-eslint/no-explicit-any function ConvertedIntegration(...rest: Parameters) { - return { - // eslint-disable-next-line @typescript-eslint/no-empty-function - setupOnce: () => {}, - ...fn(...rest), - }; + return fn(...rest); }, { id: name }, ) as unknown as Integration & { id: string; - new (...args: Parameters): Integration & - ReturnType & { - setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; - }; + new (...args: Parameters): Integration & ReturnType; }; } diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index 57c0387b25e4..6d2ec6dfc799 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -33,6 +33,8 @@ const INTEGRATION_NAME = 'InboundFilters'; const inboundFiltersIntegration: IntegrationFn = (options: Partial) => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event, _hint, client) { const clientOptions = client.getOptions(); const mergedOptions = _mergeOptions(options, clientOptions); diff --git a/packages/core/src/integrations/linkederrors.ts b/packages/core/src/integrations/linkederrors.ts index aa9df808e2d8..25e6417023f2 100644 --- a/packages/core/src/integrations/linkederrors.ts +++ b/packages/core/src/integrations/linkederrors.ts @@ -18,6 +18,8 @@ const linkedErrorsIntegration: IntegrationFn = (options: LinkedErrorsOptions = { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function preprocessEvent(event, hint, client) { const options = client.getOptions(); diff --git a/packages/core/src/integrations/metadata.ts b/packages/core/src/integrations/metadata.ts index e89cffbc8a0a..a18fc5dffebc 100644 --- a/packages/core/src/integrations/metadata.ts +++ b/packages/core/src/integrations/metadata.ts @@ -9,6 +9,8 @@ const INTEGRATION_NAME = 'ModuleMetadata'; const moduleMetadataIntegration: IntegrationFn = () => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client) { if (typeof client.on !== 'function') { return; diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index fcf70ccced1a..76d29b4751b7 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -2,6 +2,7 @@ import type { Client, IntegrationFn, Transaction } from '@sentry/types'; import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; import { addRequestDataToEvent, extractPathForTransaction } from '@sentry/utils'; import { convertIntegrationFnToClass } from '../integration'; +import { spanToJSON } from '../utils/spanUtils'; export type RequestDataIntegrationOptions = { /** @@ -70,7 +71,8 @@ const requestDataIntegration: IntegrationFn = (options: RequestDataIntegrationOp return { name: INTEGRATION_NAME, - + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event, _hint, client) { // Note: In the long run, most of the logic here should probably move into the request data utility functions. For // the moment it lives here, though, until https://github.com/getsentry/sentry-javascript/issues/5718 is addressed. @@ -105,18 +107,20 @@ const requestDataIntegration: IntegrationFn = (options: RequestDataIntegrationOp const reqWithTransaction = req as { _sentryTransaction?: Transaction }; const transaction = reqWithTransaction._sentryTransaction; if (transaction) { + const name = spanToJSON(transaction).description || ''; + // TODO (v8): Remove the nextjs check and just base it on `transactionNamingScheme` for all SDKs. (We have to // keep it the way it is for the moment, because changing the names of transactions in Sentry has the potential // to break things like alert rules.) const shouldIncludeMethodInTransactionName = getSDKName(client) === 'sentry.javascript.nextjs' - ? transaction.name.startsWith('/api') + ? name.startsWith('/api') : transactionNamingScheme !== 'path'; const [transactionValue] = extractPathForTransaction(req, { path: true, method: shouldIncludeMethodInTransactionName, - customRoute: transaction.name, + customRoute: name, }); processedEvent.transaction = transactionValue; diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index 66074a7e846c..03e81ed49f0e 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -3,6 +3,7 @@ import { logger } from '@sentry/utils'; import type { BaseClient } from '../baseclient'; import { DEBUG_BUILD } from '../debug-build'; import { getClient, getCurrentScope } from '../exports'; +import { spanToJSON } from '../utils/spanUtils'; import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants'; import { MetricsAggregator } from './integration'; import type { MetricType } from './types'; @@ -29,6 +30,7 @@ function addToMetricsAggregator( } const { unit, tags, timestamp } = data; const { release, environment } = client.getOptions(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); const metricTags: Record = {}; if (release) { @@ -38,7 +40,7 @@ function addToMetricsAggregator( metricTags.environment = environment; } if (transaction) { - metricTags.transaction = transaction.name; + metricTags.transaction = spanToJSON(transaction).description || ''; } DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`); diff --git a/packages/core/src/metrics/integration.ts b/packages/core/src/metrics/integration.ts index 531b0aa698b2..5cadf47ce3f2 100644 --- a/packages/core/src/metrics/integration.ts +++ b/packages/core/src/metrics/integration.ts @@ -8,6 +8,8 @@ const INTEGRATION_NAME = 'MetricsAggregator'; const metricsAggregatorIntegration: IntegrationFn = () => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client: BaseClient) { client.metricsAggregator = new BrowserMetricsAggregator(client); }, diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index cff498bc85a3..01546a84be25 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -24,7 +24,7 @@ import type { Transaction, User, } from '@sentry/types'; -import { dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/utils'; +import { dateTimestampInSeconds, isPlainObject, logger, uuid4 } from '@sentry/utils'; import { getGlobalEventProcessors, notifyEventProcessors } from './eventProcessors'; import { updateSession } from './session'; @@ -89,7 +89,9 @@ export class Scope implements ScopeInterface { // eslint-disable-next-line deprecation/deprecation protected _level?: Severity | SeverityLevel; - /** Transaction Name */ + /** + * Transaction Name + */ protected _transactionName?: string; /** Span */ @@ -187,10 +189,20 @@ export class Scope implements ScopeInterface { * @inheritDoc */ public setUser(user: User | null): this { - this._user = user || {}; + // If null is passed we want to unset everything, but still define keys, + // so that later down in the pipeline any existing values are cleared. + this._user = user || { + email: undefined, + id: undefined, + ip_address: undefined, + segment: undefined, + username: undefined, + }; + if (this._session) { updateSession(this._session, { user }); } + this._notifyScopeListeners(); return this; } @@ -281,7 +293,8 @@ export class Scope implements ScopeInterface { } /** - * @inheritDoc + * Sets the transaction name on the scope for future events. + * @deprecated Use extra or tags instead. */ public setTransactionName(name?: string): this { this._transactionName = name; @@ -305,7 +318,9 @@ export class Scope implements ScopeInterface { } /** - * @inheritDoc + * Sets the Span on the scope. + * @param span Span + * @deprecated Instead of setting a span on a scope, use `startSpan()`/`startSpanManual()` instead. */ public setSpan(span?: Span): this { this._span = span; @@ -314,19 +329,24 @@ export class Scope implements ScopeInterface { } /** - * @inheritDoc + * Returns the `Span` if there is one. + * @deprecated Use `getActiveSpan()` instead. */ public getSpan(): Span | undefined { return this._span; } /** - * @inheritDoc + * Returns the `Transaction` attached to the scope (if there is one). + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. */ public getTransaction(): Transaction | undefined { // Often, this span (if it exists at all) will be a transaction, but it's not guaranteed to be. Regardless, it will // have a pointer to the currently-active transaction. - const span = this.getSpan(); + const span = this._span; + // Cannot replace with getRootSpan because getRootSpan returns a span, not a transaction + // Also, this method will be removed anyway. + // eslint-disable-next-line deprecation/deprecation return span && span.transaction; } @@ -581,6 +601,90 @@ export class Scope implements ScopeInterface { return this._propagationContext; } + /** + * Capture an exception for this scope. + * + * @param exception The exception to capture. + * @param hint Optinal additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. + */ + public captureException(exception: unknown, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + + if (!this._client) { + logger.warn('No client configured on scope - will not capture exception!'); + return eventId; + } + + const syntheticException = new Error('Sentry syntheticException'); + + this._client.captureException( + exception, + { + originalException: exception, + syntheticException, + ...hint, + event_id: eventId, + }, + this, + ); + + return eventId; + } + + /** + * Capture a message for this scope. + * + * @param message The message to capture. + * @param level An optional severity level to report the message with. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured message. + */ + public captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + + if (!this._client) { + logger.warn('No client configured on scope - will not capture message!'); + return eventId; + } + + const syntheticException = new Error(message); + + this._client.captureMessage( + message, + level, + { + originalException: message, + syntheticException, + ...hint, + event_id: eventId, + }, + this, + ); + + return eventId; + } + + /** + * Captures a manually created event for this scope and sends it to Sentry. + * + * @param exception The event to capture. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured event. + */ + public captureEvent(event: Event, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + + if (!this._client) { + logger.warn('No client configured on scope - will not capture event!'); + return eventId; + } + + this._client.captureEvent(event, { ...hint, event_id: eventId }, this); + + return eventId; + } + /** * This will be called on every set call. */ diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index a7131df5528d..529f425746a7 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -30,6 +30,7 @@ export function initAndBind( } } const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation const scope = hub.getScope(); scope.update(options.initialScope); diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts new file mode 100644 index 000000000000..6239e6f6acf7 --- /dev/null +++ b/packages/core/src/semanticAttributes.ts @@ -0,0 +1,11 @@ +/** + * Use this attribute to represent the source of a span. + * Should be one of: custom, url, route, view, component, task, unknown + * + */ +export const SEMANTIC_ATTRIBUTE_SENTRY_SOURCE = 'sentry.source'; + +/** + * Use this attribute to represent the sample rate used for a span. + */ +export const SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE = 'sentry.sample_rate'; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 44b1eea5d388..aed69369fc5d 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -21,7 +21,12 @@ import { getClient } from './exports'; import { MetricsAggregator } from './metrics/aggregator'; import type { Scope } from './scope'; import { SessionFlusher } from './sessionflusher'; -import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing'; +import { + addTracingExtensions, + getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, +} from './tracing'; +import { getRootSpan } from './utils/getRootSpan'; import { spanToTraceContext } from './utils/spanUtils'; export interface ServerRuntimeClientOptions extends ClientOptions { @@ -255,9 +260,10 @@ export class ServerRuntimeClient< return [undefined, undefined]; } + // eslint-disable-next-line deprecation/deprecation const span = scope.getSpan(); if (span) { - const samplingContext = span.transaction ? span.transaction.getDynamicSamplingContext() : undefined; + const samplingContext = getRootSpan(span) ? getDynamicSamplingContextFromSpan(span) : undefined; return [samplingContext, spanToTraceContext(span)]; } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 4c82ef60ffd4..4d3786bab9c5 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,6 +1,5 @@ import type { SerializedSession, Session, SessionContext, SessionStatus } from '@sentry/types'; import { dropUndefinedKeys, timestampInSeconds, uuid4 } from '@sentry/utils'; - /** * Creates a new `Session` object by setting certain default parameters. If optional @param context * is passed, the passed properties are applied to the session object. diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index f8e8cd107c87..f27052c8cfbc 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -1,12 +1,15 @@ -import type { Client, DynamicSamplingContext, Scope } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Scope, Span, Transaction } from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; import { DEFAULT_ENVIRONMENT } from '../constants'; +import { getClient, getCurrentScope } from '../exports'; +import { getRootSpan } from '../utils/getRootSpan'; +import { spanIsSampled, spanToJSON } from '../utils/spanUtils'; /** * Creates a dynamic sampling context from a client. * - * Dispatchs the `createDsc` lifecycle hook as a side effect. + * Dispatches the `createDsc` lifecycle hook as a side effect. */ export function getDynamicSamplingContextFromClient( trace_id: string, @@ -30,3 +33,61 @@ export function getDynamicSamplingContextFromClient( return dsc; } + +/** + * A Span with a frozen dynamic sampling context. + */ +type TransactionWithV7FrozenDsc = Transaction & { _frozenDynamicSamplingContext?: DynamicSamplingContext }; + +/** + * Creates a dynamic sampling context from a span (and client and scope) + * + * @param span the span from which a few values like the root span name and sample rate are extracted. + * + * @returns a dynamic sampling context + */ +export function getDynamicSamplingContextFromSpan(span: Span): Readonly> { + const client = getClient(); + if (!client) { + return {}; + } + + // passing emit=false here to only emit later once the DSC is actually populated + const dsc = getDynamicSamplingContextFromClient(spanToJSON(span).trace_id || '', client, getCurrentScope()); + + // TODO (v8): Remove v7FrozenDsc as a Transaction will no longer have _frozenDynamicSamplingContext + const txn = getRootSpan(span) as TransactionWithV7FrozenDsc | undefined; + if (!txn) { + return dsc; + } + + // TODO (v8): Remove v7FrozenDsc as a Transaction will no longer have _frozenDynamicSamplingContext + // For now we need to avoid breaking users who directly created a txn with a DSC, where this field is still set. + // @see Transaction class constructor + const v7FrozenDsc = txn && txn._frozenDynamicSamplingContext; + if (v7FrozenDsc) { + return v7FrozenDsc; + } + + // TODO (v8): Replace txn.metadata with txn.attributes[] + // We can't do this yet because attributes aren't always set yet. + // eslint-disable-next-line deprecation/deprecation + const { sampleRate: maybeSampleRate, source } = txn.metadata; + if (maybeSampleRate != null) { + dsc.sample_rate = `${maybeSampleRate}`; + } + + // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII + const jsonSpan = spanToJSON(txn); + + // after JSON conversion, txn.name becomes jsonSpan.description + if (source && source !== 'url') { + dsc.transaction = jsonSpan.description; + } + + dsc.sampled = String(spanIsSampled(txn)); + + client.emit && client.emit('createDsc', dsc); + + return dsc; +} diff --git a/packages/core/src/tracing/errors.ts b/packages/core/src/tracing/errors.ts index 9030f02efadc..5a885dd1f090 100644 --- a/packages/core/src/tracing/errors.ts +++ b/packages/core/src/tracing/errors.ts @@ -27,6 +27,7 @@ export function registerErrorInstrumentation(): void { * If an error or unhandled promise occurs, we mark the active transaction as failed */ function errorCallback(): void { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { const status: SpanStatusType = 'internal_error'; diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts index fa4fd078b9ed..d5451e5e246b 100644 --- a/packages/core/src/tracing/hubextensions.ts +++ b/packages/core/src/tracing/hubextensions.ts @@ -12,7 +12,9 @@ import { Transaction } from './transaction'; /** Returns all trace headers that are currently on the top scope. */ function traceHeaders(this: Hub): { [key: string]: string } { + // eslint-disable-next-line deprecation/deprecation const scope = this.getScope(); + // eslint-disable-next-line deprecation/deprecation const span = scope.getSpan(); return span @@ -42,6 +44,7 @@ function _startTransaction( transactionContext: TransactionContext, customSamplingContext?: CustomSamplingContext, ): Transaction { + // eslint-disable-next-line deprecation/deprecation const client = this.getClient(); const options: Partial = (client && client.getOptions()) || {}; @@ -59,6 +62,7 @@ The transaction will not be sampled. Please use the ${configInstrumenter} instru transactionContext.sampled = false; } + // eslint-disable-next-line deprecation/deprecation let transaction = new Transaction(transactionContext, this); transaction = sampleTransaction(transaction, options, { parentSampled: transactionContext.parentSampled, @@ -86,9 +90,11 @@ export function startIdleTransaction( customSamplingContext?: CustomSamplingContext, heartbeatInterval?: number, ): IdleTransaction { + // eslint-disable-next-line deprecation/deprecation const client = hub.getClient(); const options: Partial = (client && client.getOptions()) || {}; + // eslint-disable-next-line deprecation/deprecation let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, heartbeatInterval, onScope); transaction = sampleTransaction(transaction, options, { parentSampled: transactionContext.parentSampled, diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index fdc1813e5b50..c29d82a1586e 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -45,18 +45,18 @@ export class IdleTransactionSpanRecorder extends SpanRecorder { public add(span: Span): void { // We should make sure we do not push and pop activities for // the transaction that this span recorder belongs to. - if (span.spanId !== this.transactionSpanId) { + if (span.spanContext().spanId !== this.transactionSpanId) { // We patch span.end() to pop an activity after setting an endTimestamp. // eslint-disable-next-line @typescript-eslint/unbound-method const originalEnd = span.end; span.end = (...rest: unknown[]) => { - this._popActivity(span.spanId); + this._popActivity(span.spanContext().spanId); return originalEnd.apply(span, rest); }; // We should only push new activities if the span does not have an end timestamp. if (span.endTimestamp === undefined) { - this._pushActivity(span.spanId); + this._pushActivity(span.spanContext().spanId); } } @@ -95,6 +95,9 @@ export class IdleTransaction extends Transaction { private _finishReason: (typeof IDLE_TRANSACTION_FINISH_REASONS)[number]; + /** + * @deprecated Transactions will be removed in v8. Use spans instead. + */ public constructor( transactionContext: TransactionContext, private readonly _idleHub: Hub, @@ -123,7 +126,8 @@ export class IdleTransaction extends Transaction { if (_onScope) { // We set the transaction here on the scope so error events pick up the trace // context and attach it to the error. - DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanId}`); + DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanContext().spanId}`); + // eslint-disable-next-line deprecation/deprecation _idleHub.getScope().setSpan(this); } @@ -145,7 +149,7 @@ export class IdleTransaction extends Transaction { this.activities = {}; if (this.op === 'ui.action.click') { - this.setTag(FINISH_REASON_TAG, this._finishReason); + this.setAttribute(FINISH_REASON_TAG, this._finishReason); } if (this.spanRecorder) { @@ -158,7 +162,7 @@ export class IdleTransaction extends Transaction { this.spanRecorder.spans = this.spanRecorder.spans.filter((span: Span) => { // If we are dealing with the transaction itself, we just return it - if (span.spanId === this.spanId) { + if (span.spanContext().spanId === this.spanContext().spanId) { return true; } @@ -195,8 +199,11 @@ export class IdleTransaction extends Transaction { // if `this._onScope` is `true`, the transaction put itself on the scope when it started if (this._onScope) { + // eslint-disable-next-line deprecation/deprecation const scope = this._idleHub.getScope(); + // eslint-disable-next-line deprecation/deprecation if (scope.getTransaction() === this) { + // eslint-disable-next-line deprecation/deprecation scope.setSpan(undefined); } } @@ -233,7 +240,7 @@ export class IdleTransaction extends Transaction { this._popActivity(id); }; - this.spanRecorder = new IdleTransactionSpanRecorder(pushActivity, popActivity, this.spanId, maxlen); + this.spanRecorder = new IdleTransactionSpanRecorder(pushActivity, popActivity, this.spanContext().spanId, maxlen); // Start heartbeat so that transactions do not run forever. DEBUG_BUILD && logger.log('Starting heartbeat'); diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 759a42cbdfe0..ecdc5f595095 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -19,5 +19,5 @@ export { startSpanManual, continueTrace, } from './trace'; -export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; +export { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; export { setMeasurement } from './measurement'; diff --git a/packages/core/src/tracing/measurement.ts b/packages/core/src/tracing/measurement.ts index b13bcb6b5a4a..d050a48d6029 100644 --- a/packages/core/src/tracing/measurement.ts +++ b/packages/core/src/tracing/measurement.ts @@ -6,8 +6,10 @@ import { getActiveTransaction } from './utils'; * Adds a measurement to the current active transaction. */ export function setMeasurement(name: string, value: number, unit: MeasurementUnit): void { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (transaction) { + // eslint-disable-next-line deprecation/deprecation transaction.setMeasurement(name, value, unit); } } diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 739303e1fe8c..f14aeb131db4 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -2,7 +2,9 @@ import type { Options, SamplingContext } from '@sentry/types'; import { isNaN, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; +import { spanToJSON } from '../utils/spanUtils'; import type { Transaction } from './transaction'; /** @@ -29,10 +31,8 @@ export function sampleTransaction( // if the user has forced a sampling decision by passing a `sampled` value in their transaction context, go with that // eslint-disable-next-line deprecation/deprecation if (transaction.sampled !== undefined) { - transaction.setMetadata({ - // eslint-disable-next-line deprecation/deprecation - sampleRate: Number(transaction.sampled), - }); + // eslint-disable-next-line deprecation/deprecation + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, Number(transaction.sampled)); return transaction; } @@ -41,22 +41,16 @@ export function sampleTransaction( let sampleRate; if (typeof options.tracesSampler === 'function') { sampleRate = options.tracesSampler(samplingContext); - transaction.setMetadata({ - sampleRate: Number(sampleRate), - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, Number(sampleRate)); } else if (samplingContext.parentSampled !== undefined) { sampleRate = samplingContext.parentSampled; } else if (typeof options.tracesSampleRate !== 'undefined') { sampleRate = options.tracesSampleRate; - transaction.setMetadata({ - sampleRate: Number(sampleRate), - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, Number(sampleRate)); } else { // When `enableTracing === true`, we use a sample rate of 100% sampleRate = 1; - transaction.setMetadata({ - sampleRate, - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate); } // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The @@ -100,7 +94,8 @@ export function sampleTransaction( return transaction; } - DEBUG_BUILD && logger.log(`[Tracing] starting ${transaction.op} transaction - ${transaction.name}`); + DEBUG_BUILD && + logger.log(`[Tracing] starting ${transaction.op} transaction - ${spanToJSON(transaction).description}`); return transaction; } diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index a3126e5763b0..353f2e4d694f 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -6,6 +6,8 @@ import type { SpanAttributeValue, SpanAttributes, SpanContext, + SpanContextData, + SpanJSON, SpanOrigin, SpanTimeInput, TraceContext, @@ -14,7 +16,15 @@ import type { import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; -import { spanTimeInputToSeconds, spanToTraceContext, spanToTraceHeader } from '../utils/spanUtils'; +import { getRootSpan } from '../utils/getRootSpan'; +import { + TRACE_FLAG_NONE, + TRACE_FLAG_SAMPLED, + spanTimeInputToSeconds, + spanToJSON, + spanToTraceContext, + spanToTraceHeader, +} from '../utils/spanUtils'; /** * Keeps track of finished spans for a given transaction @@ -51,16 +61,6 @@ export class SpanRecorder { * Span contains all data about a span */ export class Span implements SpanInterface { - /** - * @inheritDoc - */ - public traceId: string; - - /** - * @inheritDoc - */ - public spanId: string; - /** * @inheritDoc */ @@ -71,11 +71,6 @@ export class Span implements SpanInterface { */ public status?: SpanStatusType | string; - /** - * @inheritDoc - */ - public sampled?: boolean; - /** * Timestamp in seconds when the span was created. */ @@ -92,26 +87,18 @@ export class Span implements SpanInterface { public op?: string; /** - * @inheritDoc - */ - public description?: string; - - /** - * @inheritDoc + * Tags for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ public tags: { [key: string]: Primitive }; /** - * @inheritDoc + * Data for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any public data: { [key: string]: any }; - /** - * @inheritDoc - */ - public attributes: SpanAttributes; - /** * List of spans that were finalized */ @@ -119,11 +106,18 @@ export class Span implements SpanInterface { /** * @inheritDoc + * @deprecated Use top level `Sentry.getRootSpan()` instead */ public transaction?: Transaction; /** * The instrumenter that created this span. + * + * TODO (v8): This can probably be replaced by an `instanceOf` check of the span class. + * the instrumenter can only be sentry or otel so we can check the span instance + * to verify which one it is and remove this field entirely. + * + * @deprecated This field will be removed. */ public instrumenter: Instrumenter; @@ -132,6 +126,14 @@ export class Span implements SpanInterface { */ public origin?: SpanOrigin; + protected _traceId: string; + protected _spanId: string; + protected _sampled: boolean | undefined; + protected _name?: string; + protected _attributes: SpanAttributes; + + private _logMessage?: string; + /** * You should never call the constructor manually, always use `Sentry.startTransaction()` * or call `startChild()` on an existing span. @@ -140,32 +142,30 @@ export class Span implements SpanInterface { * @hidden */ public constructor(spanContext: SpanContext = {}) { - this.traceId = spanContext.traceId || uuid4(); - this.spanId = spanContext.spanId || uuid4().substring(16); + this._traceId = spanContext.traceId || uuid4(); + this._spanId = spanContext.spanId || uuid4().substring(16); this.startTimestamp = spanContext.startTimestamp || timestampInSeconds(); - this.tags = spanContext.tags || {}; - this.data = spanContext.data || {}; - this.attributes = spanContext.attributes || {}; + // eslint-disable-next-line deprecation/deprecation + this.tags = spanContext.tags ? { ...spanContext.tags } : {}; + // eslint-disable-next-line deprecation/deprecation + this.data = spanContext.data ? { ...spanContext.data } : {}; + this._attributes = spanContext.attributes ? { ...spanContext.attributes } : {}; + // eslint-disable-next-line deprecation/deprecation this.instrumenter = spanContext.instrumenter || 'sentry'; this.origin = spanContext.origin || 'manual'; + // eslint-disable-next-line deprecation/deprecation + this._name = spanContext.name || spanContext.description; if (spanContext.parentSpanId) { this.parentSpanId = spanContext.parentSpanId; } // We want to include booleans as well here if ('sampled' in spanContext) { - // eslint-disable-next-line deprecation/deprecation - this.sampled = spanContext.sampled; + this._sampled = spanContext.sampled; } if (spanContext.op) { this.op = spanContext.op; } - if (spanContext.description) { - this.description = spanContext.description; - } - if (spanContext.name) { - this.description = spanContext.name; - } if (spanContext.status) { this.status = spanContext.status; } @@ -174,29 +174,131 @@ export class Span implements SpanInterface { } } - /** An alias for `description` of the Span. */ + // This rule conflicts with another eslint rule :( + /* eslint-disable @typescript-eslint/member-ordering */ + + /** + * An alias for `description` of the Span. + * @deprecated Use `spanToJSON(span).description` instead. + */ public get name(): string { - return this.description || ''; + return this._name || ''; } + /** * Update the name of the span. + * @deprecated Use `spanToJSON(span).description` instead. */ public set name(name: string) { this.updateName(name); } /** - * @inheritDoc + * Get the description of the Span. + * @deprecated Use `spanToJSON(span).description` instead. + */ + public get description(): string | undefined { + return this._name; + } + + /** + * Get the description of the Span. + * @deprecated Use `spanToJSON(span).description` instead. + */ + public set description(description: string | undefined) { + this._name = description; + } + + /** + * The ID of the trace. + * @deprecated Use `spanContext().traceId` instead. + */ + public get traceId(): string { + return this._traceId; + } + + /** + * The ID of the trace. + * @deprecated You cannot update the traceId of a span after span creation. + */ + public set traceId(traceId: string) { + this._traceId = traceId; + } + + /** + * The ID of the span. + * @deprecated Use `spanContext().spanId` instead. + */ + public get spanId(): string { + return this._spanId; + } + + /** + * The ID of the span. + * @deprecated You cannot update the spanId of a span after span creation. + */ + public set spanId(spanId: string) { + this._spanId = spanId; + } + + /** + * Was this span chosen to be sent as part of the sample? + * @deprecated Use `isRecording()` instead. + */ + public get sampled(): boolean | undefined { + return this._sampled; + } + + /** + * Was this span chosen to be sent as part of the sample? + * @deprecated You cannot update the sampling decision of a span after span creation. + */ + public set sampled(sampled: boolean | undefined) { + this._sampled = sampled; + } + + /** + * Attributes for the span. + * @deprecated Use `getSpanAttributes(span)` instead. + */ + public get attributes(): SpanAttributes { + return this._attributes; + } + + /** + * Attributes for the span. + * @deprecated Use `setAttributes()` instead. + */ + public set attributes(attributes: SpanAttributes) { + this._attributes = attributes; + } + + /* eslint-enable @typescript-eslint/member-ordering */ + + /** @inheritdoc */ + public spanContext(): SpanContextData { + const { _spanId: spanId, _traceId: traceId, _sampled: sampled } = this; + return { + spanId, + traceId, + traceFlags: sampled ? TRACE_FLAG_SAMPLED : TRACE_FLAG_NONE, + }; + } + + /** + * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. + * Also the `sampled` decision will be inherited. + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ public startChild( spanContext?: Pick>, ): Span { const childSpan = new Span({ ...spanContext, - parentSpanId: this.spanId, - // eslint-disable-next-line deprecation/deprecation - sampled: this.sampled, - traceId: this.traceId, + parentSpanId: this._spanId, + sampled: this._sampled, + traceId: this._traceId, }); childSpan.spanRecorder = this.spanRecorder; @@ -204,34 +306,49 @@ export class Span implements SpanInterface { childSpan.spanRecorder.add(childSpan); } - childSpan.transaction = this.transaction; + const rootSpan = getRootSpan(this); + // TODO: still set span.transaction here until we have a more permanent solution + // Probably similarly to the weakmap we hold in node-experimental + // eslint-disable-next-line deprecation/deprecation + childSpan.transaction = rootSpan as Transaction; - if (DEBUG_BUILD && childSpan.transaction) { + if (DEBUG_BUILD && rootSpan) { const opStr = (spanContext && spanContext.op) || '< unknown op >'; - const nameStr = childSpan.transaction.name || '< unknown name >'; - const idStr = childSpan.transaction.spanId; + const nameStr = spanToJSON(childSpan).description || '< unknown name >'; + const idStr = rootSpan.spanContext().spanId; const logMessage = `[Tracing] Starting '${opStr}' span on transaction '${nameStr}' (${idStr}).`; - childSpan.transaction.metadata.spanMetadata[childSpan.spanId] = { logMessage }; logger.log(logMessage); + this._logMessage = logMessage; } return childSpan; } /** - * @inheritDoc + * Sets the tag attribute on the current span. + * + * Can also be used to unset a tag, by passing `undefined`. + * + * @param key Tag key + * @param value Tag value + * @deprecated Use `setAttribute()` instead. */ public setTag(key: string, value: Primitive): this { + // eslint-disable-next-line deprecation/deprecation this.tags = { ...this.tags, [key]: value }; return this; } /** - * @inheritDoc + * Sets the data attribute on the current span + * @param key Data key + * @param value Data value + * @deprecated Use `setAttribute()` instead. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any public setData(key: string, value: any): this { + // eslint-disable-next-line deprecation/deprecation this.data = { ...this.data, [key]: value }; return this; } @@ -240,9 +357,9 @@ export class Span implements SpanInterface { public setAttribute(key: string, value: SpanAttributeValue | undefined): void { if (value === undefined) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.attributes[key]; + delete this._attributes[key]; } else { - this.attributes[key] = value; + this._attributes[key] = value; } } @@ -263,7 +380,9 @@ export class Span implements SpanInterface { * @inheritDoc */ public setHttpStatus(httpStatus: number): this { + // eslint-disable-next-line deprecation/deprecation this.setTag('http.status_code', String(httpStatus)); + // eslint-disable-next-line deprecation/deprecation this.setData('http.response.status_code', httpStatus); const spanStatus = spanStatusfromHttpCode(httpStatus); if (spanStatus !== 'unknown_error') { @@ -272,7 +391,11 @@ export class Span implements SpanInterface { return this; } - /** @inheritdoc */ + /** + * @inheritdoc + * + * @deprecated Use `.updateName()` instead. + */ public setName(name: string): void { this.updateName(name); } @@ -281,7 +404,7 @@ export class Span implements SpanInterface { * @inheritDoc */ public updateName(name: string): this { - this.description = name; + this._name = name; return this; } @@ -303,13 +426,14 @@ export class Span implements SpanInterface { /** @inheritdoc */ public end(endTimestamp?: SpanTimeInput): void { + const rootSpan = getRootSpan(this); if ( DEBUG_BUILD && // Don't call this for transactions - this.transaction && - this.transaction.spanId !== this.spanId + rootSpan && + rootSpan.spanContext().spanId !== this._spanId ) { - const { logMessage } = this.transaction.metadata.spanMetadata[this.spanId]; + const logMessage = this._logMessage; if (logMessage) { logger.log((logMessage as string).replace('Starting', 'Finishing')); } @@ -320,6 +444,8 @@ export class Span implements SpanInterface { /** * @inheritDoc + * + * @deprecated Use `spanToTraceHeader()` instead. */ public toTraceparent(): string { return spanToTraceHeader(this); @@ -327,85 +453,90 @@ export class Span implements SpanInterface { /** * @inheritDoc + * + * @deprecated Use `spanToJSON()` or access the fields directly instead. */ public toContext(): SpanContext { return dropUndefinedKeys({ data: this._getData(), - description: this.description, + description: this._name, endTimestamp: this.endTimestamp, op: this.op, parentSpanId: this.parentSpanId, - sampled: this.sampled, - spanId: this.spanId, + sampled: this._sampled, + spanId: this._spanId, startTimestamp: this.startTimestamp, status: this.status, + // eslint-disable-next-line deprecation/deprecation tags: this.tags, - traceId: this.traceId, + traceId: this._traceId, }); } /** * @inheritDoc + * + * @deprecated Update the fields directly instead. */ public updateWithContext(spanContext: SpanContext): this { + // eslint-disable-next-line deprecation/deprecation this.data = spanContext.data || {}; - this.description = spanContext.description; + // eslint-disable-next-line deprecation/deprecation + this._name = spanContext.name || spanContext.description; this.endTimestamp = spanContext.endTimestamp; this.op = spanContext.op; this.parentSpanId = spanContext.parentSpanId; - // eslint-disable-next-line deprecation/deprecation - this.sampled = spanContext.sampled; - this.spanId = spanContext.spanId || this.spanId; + this._sampled = spanContext.sampled; + this._spanId = spanContext.spanId || this._spanId; this.startTimestamp = spanContext.startTimestamp || this.startTimestamp; this.status = spanContext.status; + // eslint-disable-next-line deprecation/deprecation this.tags = spanContext.tags || {}; - this.traceId = spanContext.traceId || this.traceId; + this._traceId = spanContext.traceId || this._traceId; return this; } /** * @inheritDoc + * + * @deprecated Use `spanToTraceContext()` util function instead. */ public getTraceContext(): TraceContext { return spanToTraceContext(this); } /** - * @inheritDoc + * Get JSON representation of this span. */ - public toJSON(): { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: { [key: string]: any }; - description?: string; - op?: string; - parent_span_id?: string; - span_id: string; - start_timestamp: number; - status?: string; - tags?: { [key: string]: Primitive }; - timestamp?: number; - trace_id: string; - origin?: SpanOrigin; - } { + public getSpanJSON(): SpanJSON { return dropUndefinedKeys({ data: this._getData(), - description: this.description, + description: this._name, op: this.op, parent_span_id: this.parentSpanId, - span_id: this.spanId, + span_id: this._spanId, start_timestamp: this.startTimestamp, status: this.status, + // eslint-disable-next-line deprecation/deprecation tags: Object.keys(this.tags).length > 0 ? this.tags : undefined, timestamp: this.endTimestamp, - trace_id: this.traceId, + trace_id: this._traceId, origin: this.origin, }); } /** @inheritdoc */ public isRecording(): boolean { - return !this.endTimestamp && !!this.sampled; + return !this.endTimestamp && !!this._sampled; + } + + /** + * Convert the object to JSON. + * @deprecated Use `spanToJSON(span)` instead. + */ + public toJSON(): SpanJSON { + return this.getSpanJSON(); } /** @@ -419,7 +550,8 @@ export class Span implements SpanInterface { [key: string]: any; } | undefined { - const { data, attributes } = this; + // eslint-disable-next-line deprecation/deprecation + const { data, _attributes: attributes } = this; const hasData = Object.keys(data).length > 0; const hasAttributes = Object.keys(attributes).length > 0; diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index d5b6aef1d1fe..015bf4757b8f 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,4 +1,15 @@ -import type { Span, SpanTimeInput, TransactionContext } from '@sentry/types'; +import type { + Instrumenter, + Primitive, + Scope, + Span, + SpanTimeInput, + TransactionContext, + TransactionMetadata, +} from '@sentry/types'; +import type { SpanAttributes } from '@sentry/types'; +import type { SpanOrigin } from '@sentry/types'; +import type { TransactionSource } from '@sentry/types'; import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -12,6 +23,100 @@ import { spanTimeInputToSeconds } from '../utils/spanUtils'; interface StartSpanOptions extends TransactionContext { /** A manually specified start time for the created `Span` object. */ startTime?: SpanTimeInput; + + /** If defined, start this span off this scope instead off the current scope. */ + scope?: Scope; + + /** The name of the span. */ + name: string; + + /** An op for the span. This is a categorization for spans. */ + op?: string; + + /** The origin of the span - if it comes from auto instrumenation or manual instrumentation. */ + origin?: SpanOrigin; + + /** Attributes for the span. */ + attributes?: SpanAttributes; + + // All remaining fields are deprecated + + /** + * @deprecated Manually set the end timestamp instead. + */ + trimEnd?: boolean; + + /** + * @deprecated This cannot be set manually anymore. + */ + parentSampled?: boolean; + + /** + * @deprecated Use attributes or set data on scopes instead. + */ + metadata?: Partial; + + /** + * The name thingy. + * @deprecated Use `name` instead. + */ + description?: string; + + /** + * @deprecated Use `span.setStatus()` instead. + */ + status?: string; + + /** + * @deprecated Use `scope` instead. + */ + parentSpanId?: string; + + /** + * @deprecated You cannot manually set the span to sampled anymore. + */ + sampled?: boolean; + + /** + * @deprecated You cannot manually set the spanId anymore. + */ + spanId?: string; + + /** + * @deprecated You cannot manually set the traceId anymore. + */ + traceId?: string; + + /** + * @deprecated Use an attribute instead. + */ + source?: TransactionSource; + + /** + * @deprecated Use attributes or set tags on the scope instead. + */ + tags?: { [key: string]: Primitive }; + + /** + * @deprecated Use attributes instead. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: { [key: string]: any }; + + /** + * @deprecated Use `startTime` instead. + */ + startTimestamp?: number; + + /** + * @deprecated Use `span.end()` instead. + */ + endTimestamp?: number; + + /** + * @deprecated You cannot set the instrumenter manually anymore. + */ + instrumenter?: Instrumenter; } /** @@ -36,14 +141,15 @@ export function trace( // eslint-disable-next-line @typescript-eslint/no-empty-function afterFinish: () => void = () => {}, ): T { - const ctx = normalizeContext(context); - const hub = getCurrentHub(); const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); + const ctx = normalizeContext(context); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); return handleCallbackErrors( @@ -54,6 +160,7 @@ export function trace( }, () => { activeSpan && activeSpan.end(); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(parentSpan); afterFinish(); }, @@ -74,11 +181,13 @@ export function trace( export function startSpan(context: StartSpanOptions, callback: (span: Span | undefined) => T): T { const ctx = normalizeContext(context); - return withScope(scope => { + return withScope(context.scope, scope => { const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); return handleCallbackErrors( @@ -116,11 +225,13 @@ export function startSpanManual( ): T { const ctx = normalizeContext(context); - return withScope(scope => { + return withScope(context.scope, scope => { const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); function finishAndSetSpan(): void { @@ -156,9 +267,13 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { const ctx = normalizeContext(context); const hub = getCurrentHub(); - const parentSpan = getActiveSpan(); + const parentSpan = context.scope + ? // eslint-disable-next-line deprecation/deprecation + context.scope.getSpan() + : getActiveSpan(); return parentSpan - ? parentSpan.startChild(ctx) + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild(ctx) : // eslint-disable-next-line deprecation/deprecation hub.startTransaction(ctx); } @@ -167,6 +282,7 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { * Returns the currently active span. */ export function getActiveSpan(): Span | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getSpan(); } @@ -240,7 +356,8 @@ function createChildSpanOrTransaction( return undefined; } return parentSpan - ? parentSpan.startChild(ctx) + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild(ctx) : // eslint-disable-next-line deprecation/deprecation hub.startTransaction(ctx); } @@ -253,16 +370,12 @@ function createChildSpanOrTransaction( * Eventually the StartSpanOptions will be more aligned with OpenTelemetry. */ function normalizeContext(context: StartSpanOptions): TransactionContext { - const ctx = { ...context }; - // If a name is set and a description is not, set the description to the name. - if (ctx.name !== undefined && ctx.description === undefined) { - ctx.description = ctx.name; - } - if (context.startTime) { + const ctx: TransactionContext & { startTime?: SpanTimeInput } = { ...context }; ctx.startTimestamp = spanTimeInputToSeconds(context.startTime); delete ctx.startTime; + return ctx; } - return ctx; + return context; } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 7142ec3419e7..d7c57f7828e8 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -15,20 +15,19 @@ import { dropUndefinedKeys, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; import { spanTimeInputToSeconds, spanToTraceContext } from '../utils/spanUtils'; -import { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; +import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { Span as SpanClass, SpanRecorder } from './span'; /** JSDoc */ export class Transaction extends SpanClass implements TransactionInterface { - public metadata: TransactionMetadata; - /** * The reference to the current hub. */ public _hub: Hub; - private _name: string; + protected _name: string; private _measurements: Measurements; @@ -36,21 +35,22 @@ export class Transaction extends SpanClass implements TransactionInterface { private _trimEnd?: boolean; + // DO NOT yet remove this property, it is used in a hack for v7 backwards compatibility. private _frozenDynamicSamplingContext: Readonly> | undefined; + private _metadata: Partial; + /** * This constructor should never be called manually. Those instrumenting tracing should use * `Sentry.startTransaction()`, and internal methods should use `hub.startTransaction()`. * @internal * @hideconstructor * @hidden + * + * @deprecated Transactions will be removed in v8. Use spans instead. */ public constructor(transactionContext: TransactionContext, hub?: Hub) { super(transactionContext); - // We need to delete description since it's set by the Span class constructor - // but not needed for transactions. - delete this.description; - this._measurements = {}; this._contexts = {}; @@ -58,47 +58,90 @@ export class Transaction extends SpanClass implements TransactionInterface { this._name = transactionContext.name || ''; - this.metadata = { - source: 'custom', + this._metadata = { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, - spanMetadata: {}, }; this._trimEnd = transactionContext.trimEnd; // this is because transactions are also spans, and spans have a transaction pointer + // TODO (v8): Replace this with another way to set the root span + // eslint-disable-next-line deprecation/deprecation this.transaction = this; // If Dynamic Sampling Context is provided during the creation of the transaction, we freeze it as it usually means // there is incoming Dynamic Sampling Context. (Either through an incoming request, a baggage meta-tag, or other means) - const incomingDynamicSamplingContext = this.metadata.dynamicSamplingContext; + const incomingDynamicSamplingContext = this._metadata.dynamicSamplingContext; if (incomingDynamicSamplingContext) { // We shallow copy this in case anything writes to the original reference of the passed in `dynamicSamplingContext` this._frozenDynamicSamplingContext = { ...incomingDynamicSamplingContext }; } } - /** Getter for `name` property */ + // This sadly conflicts with the getter/setter ordering :( + /* eslint-disable @typescript-eslint/member-ordering */ + + /** + * Getter for `name` property. + * @deprecated Use `spanToJSON(span).description` instead. + */ public get name(): string { return this._name; } /** * Setter for `name` property, which also sets `source` as custom. + * @deprecated Use `updateName()` and `setMetadata()` instead. */ public set name(newName: string) { // eslint-disable-next-line deprecation/deprecation this.setName(newName); } + /** + * Get the metadata for this transaction. + * @deprecated Use `spanGetMetadata(transaction)` instead. + */ + public get metadata(): TransactionMetadata { + // We merge attributes in for backwards compatibility + return { + // Defaults + // eslint-disable-next-line deprecation/deprecation + source: 'custom', + spanMetadata: {}, + + // Legacy metadata + ...this._metadata, + + // From attributes + ...(this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] && { + source: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionMetadata['source'], + }), + ...(this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] && { + sampleRate: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as TransactionMetadata['sampleRate'], + }), + }; + } + + /** + * Update the metadata for this transaction. + * @deprecated Use `spanGetMetadata(transaction)` instead. + */ + public set metadata(metadata: TransactionMetadata) { + this._metadata = metadata; + } + + /* eslint-enable @typescript-eslint/member-ordering */ + /** * Setter for `name` property, which also sets `source` on the metadata. * - * @deprecated Use `updateName()` and `setMetadata()` instead. + * @deprecated Use `.updateName()` and `.setAttribute()` instead. */ public setName(name: string, source: TransactionMetadata['source'] = 'custom'): void { this._name = name; - this.metadata.source = source; + this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } /** @inheritdoc */ @@ -119,7 +162,8 @@ export class Transaction extends SpanClass implements TransactionInterface { } /** - * @inheritDoc + * Set the context of a transaction event. + * @deprecated Use either `.setAttribute()`, or set the context on the scope before creating the transaction. */ public setContext(key: string, context: Context | null): void { if (context === null) { @@ -132,16 +176,19 @@ export class Transaction extends SpanClass implements TransactionInterface { /** * @inheritDoc + * + * @deprecated Use top-level `setMeasurement()` instead. */ public setMeasurement(name: string, value: number, unit: MeasurementUnit = ''): void { this._measurements[name] = { value, unit }; } /** - * @inheritDoc + * Store metadata on this transaction. + * @deprecated Use attributes or store data on the scope instead. */ public setMetadata(newMetadata: Partial): void { - this.metadata = { ...this.metadata, ...newMetadata }; + this._metadata = { ...this._metadata, ...newMetadata }; } /** @@ -153,6 +200,7 @@ export class Transaction extends SpanClass implements TransactionInterface { if (!transaction) { return undefined; } + // eslint-disable-next-line deprecation/deprecation return this._hub.captureEvent(transaction); } @@ -165,7 +213,7 @@ export class Transaction extends SpanClass implements TransactionInterface { return dropUndefinedKeys({ ...spanContext, - name: this.name, + name: this._name, trimEnd: this._trimEnd, }); } @@ -177,8 +225,7 @@ export class Transaction extends SpanClass implements TransactionInterface { // eslint-disable-next-line deprecation/deprecation super.updateWithContext(transactionContext); - this.name = transactionContext.name || ''; - + this._name = transactionContext.name || ''; this._trimEnd = transactionContext.trimEnd; return this; @@ -188,39 +235,11 @@ export class Transaction extends SpanClass implements TransactionInterface { * @inheritdoc * * @experimental + * + * @deprecated Use top-level `getDynamicSamplingContextFromSpan` instead. */ public getDynamicSamplingContext(): Readonly> { - if (this._frozenDynamicSamplingContext) { - return this._frozenDynamicSamplingContext; - } - - const hub = this._hub || getCurrentHub(); - const client = hub.getClient(); - - if (!client) return {}; - - const scope = hub.getScope(); - const dsc = getDynamicSamplingContextFromClient(this.traceId, client, scope); - - const maybeSampleRate = this.metadata.sampleRate; - if (maybeSampleRate !== undefined) { - dsc.sample_rate = `${maybeSampleRate}`; - } - - // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII - const source = this.metadata.source; - if (source && source !== 'url') { - dsc.transaction = this.name; - } - - if (this.sampled !== undefined) { - dsc.sampled = String(this.sampled); - } - - // Uncomment if we want to make DSC immutable - // this._frozenDynamicSamplingContext = dsc; - - return dsc; + return getDynamicSamplingContextFromSpan(this); } /** @@ -242,20 +261,21 @@ export class Transaction extends SpanClass implements TransactionInterface { return undefined; } - if (!this.name) { + if (!this._name) { DEBUG_BUILD && logger.warn('Transaction has no name, falling back to ``.'); - this.name = ''; + this._name = ''; } // just sets the end timestamp super.end(endTimestamp); + // eslint-disable-next-line deprecation/deprecation const client = this._hub.getClient(); if (client && client.emit) { client.emit('finishTransaction', this); } - if (this.sampled !== true) { + if (this._sampled !== true) { // At this point if `sampled !== true` we want to discard the transaction. DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); @@ -277,7 +297,10 @@ export class Transaction extends SpanClass implements TransactionInterface { }).endTimestamp; } - const metadata = this.metadata; + // eslint-disable-next-line deprecation/deprecation + const { metadata } = this; + // eslint-disable-next-line deprecation/deprecation + const { source } = metadata; const transaction: TransactionEvent = { contexts: { @@ -285,19 +308,21 @@ export class Transaction extends SpanClass implements TransactionInterface { // We don't want to override trace context trace: spanToTraceContext(this), }, + // TODO: Pass spans serialized via `spanToJSON()` here instead in v8. spans: finishedSpans, start_timestamp: this.startTimestamp, + // eslint-disable-next-line deprecation/deprecation tags: this.tags, timestamp: this.endTimestamp, - transaction: this.name, + transaction: this._name, type: 'transaction', sdkProcessingMetadata: { ...metadata, - dynamicSamplingContext: this.getDynamicSamplingContext(), + dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), }, - ...(metadata.source && { + ...(source && { transaction_info: { - source: metadata.source, + source, }, }), }; @@ -313,7 +338,7 @@ export class Transaction extends SpanClass implements TransactionInterface { transaction.measurements = this._measurements; } - DEBUG_BUILD && logger.log(`[Tracing] Finishing ${this.op} transaction: ${this.name}.`); + DEBUG_BUILD && logger.log(`[Tracing] Finishing ${this.op} transaction: ${this._name}.`); return transaction; } diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index f1b4c0f1ae06..13c2cded52e4 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -4,10 +4,16 @@ import { extractTraceparentData as _extractTraceparentData } from '@sentry/utils import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; -/** Grabs active transaction off scope, if any */ +/** + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. + */ export function getActiveTransaction(maybeHub?: Hub): T | undefined { const hub = maybeHub || getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation return scope.getTransaction() as T | undefined; } diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index 96a85740ef64..bdf60497cff1 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -1,6 +1,8 @@ import type { Breadcrumb, Event, PropagationContext, ScopeData, Span } from '@sentry/types'; -import { arrayify } from '@sentry/utils'; -import { spanToTraceContext } from './spanUtils'; +import { arrayify, dropUndefinedKeys } from '@sentry/utils'; +import { getDynamicSamplingContextFromSpan } from '../tracing/dynamicSamplingContext'; +import { getRootSpan } from './getRootSpan'; +import { spanToJSON, spanToTraceContext } from './spanUtils'; /** * Applies data from the scope to the event and runs all event processors on it. @@ -37,21 +39,23 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { eventProcessors, attachments, propagationContext, + // eslint-disable-next-line deprecation/deprecation transactionName, span, } = mergeData; - mergePropOverwrite(data, 'extra', extra); - mergePropOverwrite(data, 'tags', tags); - mergePropOverwrite(data, 'user', user); - mergePropOverwrite(data, 'contexts', contexts); - mergePropOverwrite(data, 'sdkProcessingMetadata', sdkProcessingMetadata); + mergeAndOverwriteScopeData(data, 'extra', extra); + mergeAndOverwriteScopeData(data, 'tags', tags); + mergeAndOverwriteScopeData(data, 'user', user); + mergeAndOverwriteScopeData(data, 'contexts', contexts); + mergeAndOverwriteScopeData(data, 'sdkProcessingMetadata', sdkProcessingMetadata); if (level) { data.level = level; } if (transactionName) { + // eslint-disable-next-line deprecation/deprecation data.transactionName = transactionName; } @@ -79,28 +83,21 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { } /** - * Merge properties, overwriting existing keys. + * Merges certain scope data. Undefined values will overwrite any existing values. * Exported only for tests. */ -export function mergePropOverwrite< +export function mergeAndOverwriteScopeData< Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', - Data extends ScopeData | Event, + Data extends ScopeData, >(data: Data, prop: Prop, mergeVal: Data[Prop]): void { if (mergeVal && Object.keys(mergeVal).length) { - data[prop] = { ...data[prop], ...mergeVal }; - } -} - -/** - * Merge properties, keeping existing keys. - * Exported only for tests. - */ -export function mergePropKeep< - Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', - Data extends ScopeData | Event, ->(data: Data, prop: Prop, mergeVal: Data[Prop]): void { - if (mergeVal && Object.keys(mergeVal).length) { - data[prop] = { ...mergeVal, ...data[prop] }; + // Clone object + data[prop] = { ...data[prop] }; + for (const key in mergeVal) { + if (Object.prototype.hasOwnProperty.call(mergeVal, key)) { + data[prop][key] = mergeVal[key]; + } + } } } @@ -122,23 +119,40 @@ export function mergeArray( } function applyDataToEvent(event: Event, data: ScopeData): void { - const { extra, tags, user, contexts, level, transactionName } = data; + const { + extra, + tags, + user, + contexts, + level, + // eslint-disable-next-line deprecation/deprecation + transactionName, + } = data; - if (extra && Object.keys(extra).length) { - event.extra = { ...extra, ...event.extra }; + const cleanedExtra = dropUndefinedKeys(extra); + if (cleanedExtra && Object.keys(cleanedExtra).length) { + event.extra = { ...cleanedExtra, ...event.extra }; } - if (tags && Object.keys(tags).length) { - event.tags = { ...tags, ...event.tags }; + + const cleanedTags = dropUndefinedKeys(tags); + if (cleanedTags && Object.keys(cleanedTags).length) { + event.tags = { ...cleanedTags, ...event.tags }; } - if (user && Object.keys(user).length) { - event.user = { ...user, ...event.user }; + + const cleanedUser = dropUndefinedKeys(user); + if (cleanedUser && Object.keys(cleanedUser).length) { + event.user = { ...cleanedUser, ...event.user }; } - if (contexts && Object.keys(contexts).length) { - event.contexts = { ...contexts, ...event.contexts }; + + const cleanedContexts = dropUndefinedKeys(contexts); + if (cleanedContexts && Object.keys(cleanedContexts).length) { + event.contexts = { ...cleanedContexts, ...event.contexts }; } + if (level) { event.level = level; } + if (transactionName) { event.transaction = transactionName; } @@ -163,13 +177,13 @@ function applySdkMetadataToEvent( function applySpanToEvent(event: Event, span: Span): void { event.contexts = { trace: spanToTraceContext(span), ...event.contexts }; - const transaction = span.transaction; - if (transaction) { + const rootSpan = getRootSpan(span); + if (rootSpan) { event.sdkProcessingMetadata = { - dynamicSamplingContext: transaction.getDynamicSamplingContext(), + dynamicSamplingContext: getDynamicSamplingContextFromSpan(span), ...event.sdkProcessingMetadata, }; - const transactionName = transaction.name; + const transactionName = spanToJSON(rootSpan).description; if (transactionName) { event.tags = { transaction: transactionName, ...event.tags }; } diff --git a/packages/core/src/utils/getRootSpan.ts b/packages/core/src/utils/getRootSpan.ts new file mode 100644 index 000000000000..9a0f5d642a77 --- /dev/null +++ b/packages/core/src/utils/getRootSpan.ts @@ -0,0 +1,15 @@ +import type { Span } from '@sentry/types'; + +/** + * Returns the root span of a given span. + * + * As long as we use `Transaction`s internally, the returned root span + * will be a `Transaction` but be aware that this might change in the future. + * + * If the given span has no root span or transaction, `undefined` is returned. + */ +export function getRootSpan(span: Span): Span | undefined { + // TODO (v8): Remove this check and just return span + // eslint-disable-next-line deprecation/deprecation + return span.transaction; +} diff --git a/packages/core/src/utils/isSentryRequestUrl.ts b/packages/core/src/utils/isSentryRequestUrl.ts index 3a31f63cf46c..acc6b0d68cab 100644 --- a/packages/core/src/utils/isSentryRequestUrl.ts +++ b/packages/core/src/utils/isSentryRequestUrl.ts @@ -7,7 +7,11 @@ import type { Client, DsnComponents, Hub } from '@sentry/types'; * TODO(v8): Remove Hub fallback type */ export function isSentryRequestUrl(url: string, hubOrClient: Hub | Client | undefined): boolean { - const client = hubOrClient && isHub(hubOrClient) ? hubOrClient.getClient() : hubOrClient; + const client = + hubOrClient && isHub(hubOrClient) + ? // eslint-disable-next-line deprecation/deprecation + hubOrClient.getClient() + : hubOrClient; const dsn = client && client.getDsn(); const tunnel = client && client.getOptions().tunnel; @@ -31,5 +35,6 @@ function removeTrailingSlash(str: string): string { } function isHub(hubOrClient: Hub | Client | undefined): hubOrClient is Hub { + // eslint-disable-next-line deprecation/deprecation return (hubOrClient as Hub).getClient !== undefined; } diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 6f448df8496d..17be8c5354de 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -15,6 +15,7 @@ import { DEFAULT_ENVIRONMENT } from '../constants'; import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors'; import { Scope, getGlobalScope } from '../scope'; import { applyScopeDataToEvent, mergeScopeData } from './applyScopeDataToEvent'; +import { spanToJSON } from './spanUtils'; /** * This type makes sure that we get either a CaptureContext, OR an EventHint. @@ -326,10 +327,14 @@ function normalizeEvent(event: Event | null, depth: number, maxBreadth: number): // event.spans[].data may contain circular/dangerous data so we need to normalize it if (event.spans) { normalized.spans = event.spans.map(span => { - // We cannot use the spread operator here because `toJSON` on `span` is non-enumerable - if (span.data) { - span.data = normalize(span.data, depth, maxBreadth); + const data = spanToJSON(span).data; + + if (data) { + // This is a bit weird, as we generally have `Span` instances here, but to be safe we do not assume so + // eslint-disable-next-line deprecation/deprecation + span.data = normalize(data, depth, maxBreadth); } + return span; }); } diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 9c4dcff17b62..cbf8ce6d7f5b 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -1,15 +1,20 @@ -import type { Span, SpanTimeInput, TraceContext } from '@sentry/types'; +import type { Span, SpanJSON, SpanTimeInput, TraceContext } from '@sentry/types'; import { dropUndefinedKeys, generateSentryTraceHeader, timestampInSeconds } from '@sentry/utils'; +import type { Span as SpanClass } from '../tracing/span'; + +// These are aligned with OpenTelemetry trace flags +export const TRACE_FLAG_NONE = 0x0; +export const TRACE_FLAG_SAMPLED = 0x1; /** * Convert a span to a trace context, which can be sent as the `trace` context in an event. */ export function spanToTraceContext(span: Span): TraceContext { - const { data, description, op, parent_span_id, span_id, status, tags, trace_id, origin } = span.toJSON(); + const { spanId: span_id, traceId: trace_id } = span.spanContext(); + const { data, op, parent_span_id, status, tags, origin } = spanToJSON(span); return dropUndefinedKeys({ data, - description, op, parent_span_id, span_id, @@ -24,7 +29,9 @@ export function spanToTraceContext(span: Span): TraceContext { * Convert a Span to a Sentry trace header. */ export function spanToTraceHeader(span: Span): string { - return generateSentryTraceHeader(span.traceId, span.spanId, span.isRecording()); + const { traceId, spanId } = span.spanContext(); + const sampled = spanIsSampled(span); + return generateSentryTraceHeader(traceId, spanId, sampled); } /** @@ -54,3 +61,49 @@ function ensureTimestampInSeconds(timestamp: number): number { const isMs = timestamp > 9999999999; return isMs ? timestamp / 1000 : timestamp; } + +/** + * Convert a span to a JSON representation. + * Note that all fields returned here are optional and need to be guarded against. + * + * Note: Because of this, we currently have a circular type dependency (which we opted out of in package.json). + * This is not avoidable as we need `spanToJSON` in `spanUtils.ts`, which in turn is needed by `span.ts` for backwards compatibility. + * And `spanToJSON` needs the Span class from `span.ts` to check here. + * TODO v8: When we remove the deprecated stuff from `span.ts`, we can remove the circular dependency again. + */ +export function spanToJSON(span: Span): Partial { + if (spanIsSpanClass(span)) { + return span.getSpanJSON(); + } + + // Fallback: We also check for `.toJSON()` here... + // eslint-disable-next-line deprecation/deprecation + if (typeof span.toJSON === 'function') { + // eslint-disable-next-line deprecation/deprecation + return span.toJSON(); + } + + return {}; +} + +/** + * Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof. + * :( So instead we approximate this by checking if it has the `getSpanJSON` method. + */ +function spanIsSpanClass(span: Span): span is SpanClass { + return typeof (span as SpanClass).getSpanJSON === 'function'; +} + +/** + * Returns true if a span is sampled. + * In most cases, you should just use `span.isRecording()` instead. + * However, this has a slightly different semantic, as it also returns false if the span is finished. + * So in the case where this distinction is important, use this method. + */ +export function spanIsSampled(span: Span): boolean { + // We align our trace flags with the ones OpenTelemetry use + // So we also check for sampled the same way they do. + const { traceFlags } = span.spanContext(); + // eslint-disable-next-line no-bitwise + return Boolean(traceFlags & TRACE_FLAG_SAMPLED); +} diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index d56d0404ab87..bfdb07bd9a83 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1 +1 @@ -export const SDK_VERSION = '7.92.0'; +export const SDK_VERSION = '7.93.0'; diff --git a/packages/core/test/lib/async-context.test.ts b/packages/core/test/lib/async-context.test.ts new file mode 100644 index 000000000000..58b44c922b09 --- /dev/null +++ b/packages/core/test/lib/async-context.test.ts @@ -0,0 +1,13 @@ +import { getCurrentHub, runWithAsyncContext } from '../../src'; + +describe('runWithAsyncContext()', () => { + it('without strategy hubs should be equal', () => { + runWithAsyncContext(() => { + const hub1 = getCurrentHub(); + runWithAsyncContext(() => { + const hub2 = getCurrentHub(); + expect(hub1).toBe(hub2); + }); + }); + }); +}); diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 0b39216e19a5..197ffa189779 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -122,6 +122,7 @@ describe('BaseClient', () => { const hub = new Hub(client, scope); scope.addBreadcrumb({ message: 'hello' }, 100); + // eslint-disable-next-line deprecation/deprecation hub.addBreadcrumb({ message: 'world' }); expect((scope as any)._breadcrumbs[1].message).toEqual('world'); @@ -136,6 +137,7 @@ describe('BaseClient', () => { const hub = new Hub(client, scope); scope.addBreadcrumb({ message: 'hello' }, 100); + // eslint-disable-next-line deprecation/deprecation hub.addBreadcrumb({ message: 'world' }); expect((scope as any)._breadcrumbs[1].timestamp).toBeGreaterThan(1); @@ -150,6 +152,7 @@ describe('BaseClient', () => { const hub = new Hub(client, scope); scope.addBreadcrumb({ message: 'hello' }, 100); + // eslint-disable-next-line deprecation/deprecation hub.addBreadcrumb({ message: 'world' }); expect((scope as any)._breadcrumbs.length).toEqual(1); @@ -164,7 +167,8 @@ describe('BaseClient', () => { const scope = new Scope(); const hub = new Hub(client, scope); - hub.addBreadcrumb({ message: 'hello' }); + scope.addBreadcrumb({ message: 'hello' }); + // eslint-disable-next-line deprecation/deprecation hub.addBreadcrumb({ message: 'world' }); expect((scope as any)._breadcrumbs).toHaveLength(2); @@ -179,6 +183,7 @@ describe('BaseClient', () => { const scope = new Scope(); const hub = new Hub(client, scope); + // eslint-disable-next-line deprecation/deprecation hub.addBreadcrumb({ message: 'hello' }); expect((scope as any)._breadcrumbs[0].message).toEqual('hello'); @@ -193,6 +198,7 @@ describe('BaseClient', () => { const scope = new Scope(); const hub = new Hub(client, scope); + // eslint-disable-next-line deprecation/deprecation hub.addBreadcrumb({ message: 'hello' }); expect((scope as any)._breadcrumbs[0].message).toEqual('changed'); @@ -207,6 +213,7 @@ describe('BaseClient', () => { const scope = new Scope(); const hub = new Hub(client, scope); + // eslint-disable-next-line deprecation/deprecation hub.addBreadcrumb({ message: 'hello' }); expect((scope as any)._breadcrumbs.length).toEqual(0); @@ -221,6 +228,7 @@ describe('BaseClient', () => { const scope = new Scope(); const hub = new Hub(client, scope); + // eslint-disable-next-line deprecation/deprecation hub.addBreadcrumb({ message: 'hello' }, { data: 'someRandomThing' }); expect((scope as any)._breadcrumbs[0].message).toEqual('hello'); @@ -613,7 +621,9 @@ describe('BaseClient', () => { const client = new TestClient(options); const scope = new Scope(); const hub = new Hub(client, scope); + // eslint-disable-next-line deprecation/deprecation hub.addBreadcrumb({ message: '1' }); + // eslint-disable-next-line deprecation/deprecation hub.addBreadcrumb({ message: '2' }); client.captureEvent({ message: 'message' }, undefined, scope); @@ -671,7 +681,7 @@ describe('BaseClient', () => { test('adds installed integrations to sdk info', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); const client = new TestClient(options); - client.setupIntegrations(); + client.init(); client.captureEvent({ message: 'message' }); @@ -685,7 +695,7 @@ describe('BaseClient', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); const client = new TestClient(options); - client.setupIntegrations(); + client.init(); client.addIntegration(new AdHocIntegration()); client.captureException(new Error('test exception')); @@ -706,7 +716,7 @@ describe('BaseClient', () => { integrations: [new TestIntegration(), null, undefined], }); const client = new TestClient(options); - client.setupIntegrations(); + client.init(); client.captureEvent({ message: 'message' }); @@ -1482,24 +1492,48 @@ describe('BaseClient', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); const client = new TestClient(options); + // eslint-disable-next-line deprecation/deprecation client.setupIntegrations(); expect(Object.keys((client as any)._integrations).length).toEqual(1); - expect(client.getIntegration(TestIntegration)).toBeTruthy(); + expect(client.getIntegrationByName(TestIntegration.id)).toBeTruthy(); + }); + + test('sets up each integration on `init` call', () => { + expect.assertions(2); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); + const client = new TestClient(options); + client.init(); + + expect(Object.keys((client as any)._integrations).length).toEqual(1); + expect(client.getIntegrationByName(TestIntegration.id)).toBeTruthy(); }); - test('skips installation if DSN is not provided', () => { + test('skips installation for `setupIntegrations()` if DSN is not provided', () => { expect.assertions(2); const options = getDefaultTestClientOptions({ integrations: [new TestIntegration()] }); const client = new TestClient(options); + // eslint-disable-next-line deprecation/deprecation client.setupIntegrations(); expect(Object.keys((client as any)._integrations).length).toEqual(0); - expect(client.getIntegration(TestIntegration)).toBeFalsy(); + expect(client.getIntegrationByName(TestIntegration.id)).toBeFalsy(); }); - test('skips installation if `enabled` is set to `false`', () => { + test('skips installation for `init()` if DSN is not provided', () => { + expect.assertions(2); + + const options = getDefaultTestClientOptions({ integrations: [new TestIntegration()] }); + const client = new TestClient(options); + client.init(); + + expect(Object.keys((client as any)._integrations).length).toEqual(0); + expect(client.getIntegrationByName(TestIntegration.id)).toBeFalsy(); + }); + + test('skips installation for `setupIntegrations()` if `enabled` is set to `false`', () => { expect.assertions(2); const options = getDefaultTestClientOptions({ @@ -1508,10 +1542,26 @@ describe('BaseClient', () => { integrations: [new TestIntegration()], }); const client = new TestClient(options); + // eslint-disable-next-line deprecation/deprecation client.setupIntegrations(); expect(Object.keys((client as any)._integrations).length).toEqual(0); - expect(client.getIntegration(TestIntegration)).toBeFalsy(); + expect(client.getIntegrationByName(TestIntegration.id)).toBeFalsy(); + }); + + test('skips installation for `init()` if `enabled` is set to `false`', () => { + expect.assertions(2); + + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enabled: false, + integrations: [new TestIntegration()], + }); + const client = new TestClient(options); + client.init(); + + expect(Object.keys((client as any)._integrations).length).toEqual(0); + expect(client.getIntegrationByName(TestIntegration.id)).toBeFalsy(); }); test('skips installation if integrations are already installed', () => { @@ -1523,17 +1573,41 @@ describe('BaseClient', () => { const setupIntegrationsHelper = jest.spyOn(integrationModule, 'setupIntegrations'); // it should install the first time, because integrations aren't yet installed... + // eslint-disable-next-line deprecation/deprecation client.setupIntegrations(); expect(Object.keys((client as any)._integrations).length).toEqual(1); - expect(client.getIntegration(TestIntegration)).toBeTruthy(); + expect(client.getIntegrationByName(TestIntegration.id)).toBeTruthy(); expect(setupIntegrationsHelper).toHaveBeenCalledTimes(1); // ...but it shouldn't try to install a second time + // eslint-disable-next-line deprecation/deprecation client.setupIntegrations(); expect(setupIntegrationsHelper).toHaveBeenCalledTimes(1); }); + + test('does not add integrations twice when calling `init` multiple times', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); + const client = new TestClient(options); + // note: not the `Client` method `setupIntegrations`, but the free-standing function which that method calls + const setupIntegrationsHelper = jest.spyOn(integrationModule, 'setupIntegrations'); + + // it should install the first time, because integrations aren't yet installed... + client.init(); + + expect(Object.keys((client as any)._integrations).length).toEqual(1); + expect(client.getIntegrationByName(TestIntegration.id)).toBeTruthy(); + expect(setupIntegrationsHelper).toHaveBeenCalledTimes(1); + + client.init(); + + // is called again... + expect(setupIntegrationsHelper).toHaveBeenCalledTimes(2); + + // but integrations are only added once anyhow! + expect(client['_integrations']).toEqual({ TestIntegration: expect.any(TestIntegration) }); + }); }); describe('flush/close', () => { diff --git a/packages/core/test/lib/exports.test.ts b/packages/core/test/lib/exports.test.ts index 7a4ec6987dd1..e4512b7f6732 100644 --- a/packages/core/test/lib/exports.test.ts +++ b/packages/core/test/lib/exports.test.ts @@ -1,4 +1,21 @@ -import { Hub, Scope, getCurrentScope, makeMain, withScope } from '../../src'; +import { + Hub, + Scope, + captureSession, + endSession, + getCurrentScope, + getIsolationScope, + makeMain, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, + startSession, + withIsolationScope, + withScope, +} from '../../src'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; function getTestClient(): TestClient { @@ -124,4 +141,175 @@ describe('withScope', () => { expect(getCurrentScope()).toBe(scope1); }); + + it('allows to pass a custom scope', () => { + const scope1 = getCurrentScope(); + scope1.setExtra('x1', 'x1'); + + const customScope = new Scope(); + customScope.setExtra('x2', 'x2'); + + withScope(customScope, scope2 => { + expect(scope2).not.toBe(scope1); + expect(scope2).toBe(customScope); + expect(getCurrentScope()).toBe(scope2); + expect(scope2['_extra']).toEqual({ x2: 'x2' }); + }); + + withScope(customScope, scope3 => { + expect(scope3).not.toBe(scope1); + expect(scope3).toBe(customScope); + expect(getCurrentScope()).toBe(scope3); + expect(scope3['_extra']).toEqual({ x2: 'x2' }); + }); + + expect(getCurrentScope()).toBe(scope1); + }); +}); + +describe('session APIs', () => { + beforeEach(() => { + const client = getTestClient(); + const hub = new Hub(client); + makeMain(hub); + }); + + describe('startSession', () => { + it('starts a session', () => { + const session = startSession(); + + expect(session).toMatchObject({ + status: 'ok', + errors: 0, + init: true, + environment: 'production', + ignoreDuration: false, + sid: expect.any(String), + did: undefined, + timestamp: expect.any(Number), + started: expect.any(Number), + duration: expect.any(Number), + toJSON: expect.any(Function), + }); + }); + + it('ends a previously active session and removes it from the scope', () => { + const session1 = startSession(); + + expect(session1.status).toBe('ok'); + expect(getIsolationScope().getSession()).toBe(session1); + + const session2 = startSession(); + + expect(session2.status).toBe('ok'); + expect(session1.status).toBe('exited'); + expect(getIsolationScope().getSession()).toBe(session2); + }); + }); + + describe('endSession', () => { + it('ends a session and removes it from the scope', () => { + const session = startSession(); + + expect(session.status).toBe('ok'); + expect(getIsolationScope().getSession()).toBe(session); + + endSession(); + + expect(session.status).toBe('exited'); + expect(getIsolationScope().getSession()).toBe(undefined); + }); + }); + + describe('captureSession', () => { + it('captures a session without ending it by default', () => { + const session = startSession({ release: '1.0.0' }); + + expect(session.status).toBe('ok'); + expect(session.init).toBe(true); + expect(getIsolationScope().getSession()).toBe(session); + + captureSession(); + + // this flag indicates the session was sent via BaseClient + expect(session.init).toBe(false); + + // session is still active and on the scope + expect(session.status).toBe('ok'); + expect(getIsolationScope().getSession()).toBe(session); + }); + + it('captures a session and ends it if end is `true`', () => { + const session = startSession({ release: '1.0.0' }); + + expect(session.status).toBe('ok'); + expect(session.init).toBe(true); + expect(getIsolationScope().getSession()).toBe(session); + + captureSession(true); + + // this flag indicates the session was sent via BaseClient + expect(session.init).toBe(false); + + // session is still active and on the scope + expect(session.status).toBe('exited'); + expect(getIsolationScope().getSession()).toBe(undefined); + }); + }); + + describe('setUser', () => { + it('should write to the isolation scope', () => { + withIsolationScope(isolationScope => { + setUser({ id: 'foo' }); + expect(isolationScope.getScopeData().user.id).toBe('foo'); + }); + }); + }); + + describe('setTags', () => { + it('should write to the isolation scope', () => { + withIsolationScope(isolationScope => { + setTags({ wee: true, woo: false }); + expect(isolationScope.getScopeData().tags['wee']).toBe(true); + expect(isolationScope.getScopeData().tags['woo']).toBe(false); + }); + }); + }); + + describe('setTag', () => { + it('should write to the isolation scope', () => { + withIsolationScope(isolationScope => { + setTag('foo', true); + expect(isolationScope.getScopeData().tags['foo']).toBe(true); + }); + }); + }); + + describe('setExtras', () => { + it('should write to the isolation scope', () => { + withIsolationScope(isolationScope => { + setExtras({ wee: { foo: 'bar' }, woo: { foo: 'bar' } }); + expect(isolationScope.getScopeData().extra?.wee).toEqual({ foo: 'bar' }); + expect(isolationScope.getScopeData().extra?.woo).toEqual({ foo: 'bar' }); + }); + }); + }); + + describe('setExtra', () => { + it('should write to the isolation scope', () => { + withIsolationScope(isolationScope => { + setExtra('foo', { bar: 'baz' }); + expect(isolationScope.getScopeData().extra?.foo).toEqual({ bar: 'baz' }); + }); + }); + }); + + describe('setContext', () => { + it('should write to the isolation scope', () => { + withIsolationScope(isolationScope => { + setContext('foo', { bar: 'baz' }); + expect(isolationScope.getScopeData().contexts?.foo?.bar).toBe('baz'); + }); + }); + }); }); diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 137a7dce4df3..829ef4de991b 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -377,7 +377,7 @@ describe('setupIntegration', () => { setupIntegration(client2, integration3, integrationIndex); setupIntegration(client2, integration4, integrationIndex); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex).toEqual({ test: integration1 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); @@ -394,32 +394,32 @@ describe('setupIntegration', () => { const client1 = getTestClient(); const client2 = getTestClient(); - const integrationIndex = {}; + const integrationIndex1 = {}; + const integrationIndex2 = {}; const integration1 = new CustomIntegration(); const integration2 = new CustomIntegration(); const integration3 = new CustomIntegration(); const integration4 = new CustomIntegration(); - setupIntegration(client1, integration1, integrationIndex); - setupIntegration(client1, integration2, integrationIndex); - setupIntegration(client2, integration3, integrationIndex); - setupIntegration(client2, integration4, integrationIndex); + setupIntegration(client1, integration1, integrationIndex1); + setupIntegration(client1, integration2, integrationIndex1); + setupIntegration(client2, integration3, integrationIndex2); + setupIntegration(client2, integration4, integrationIndex2); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex1).toEqual({ test: integration1 }); + expect(integrationIndex2).toEqual({ test: integration3 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); expect(integration4.setupOnce).not.toHaveBeenCalled(); expect(integration1.setup).toHaveBeenCalledTimes(1); - expect(integration2.setup).toHaveBeenCalledTimes(1); + expect(integration2.setup).toHaveBeenCalledTimes(0); expect(integration3.setup).toHaveBeenCalledTimes(1); - expect(integration4.setup).toHaveBeenCalledTimes(1); + expect(integration4.setup).toHaveBeenCalledTimes(0); expect(integration1.setup).toHaveBeenCalledWith(client1); - expect(integration2.setup).toHaveBeenCalledWith(client1); expect(integration3.setup).toHaveBeenCalledWith(client2); - expect(integration4.setup).toHaveBeenCalledWith(client2); }); it('binds preprocessEvent for each client', () => { @@ -432,18 +432,20 @@ describe('setupIntegration', () => { const client1 = getTestClient(); const client2 = getTestClient(); - const integrationIndex = {}; + const integrationIndex1 = {}; + const integrationIndex2 = {}; const integration1 = new CustomIntegration(); const integration2 = new CustomIntegration(); const integration3 = new CustomIntegration(); const integration4 = new CustomIntegration(); - setupIntegration(client1, integration1, integrationIndex); - setupIntegration(client1, integration2, integrationIndex); - setupIntegration(client2, integration3, integrationIndex); - setupIntegration(client2, integration4, integrationIndex); + setupIntegration(client1, integration1, integrationIndex1); + setupIntegration(client1, integration2, integrationIndex1); + setupIntegration(client2, integration3, integrationIndex2); + setupIntegration(client2, integration4, integrationIndex2); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex1).toEqual({ test: integration1 }); + expect(integrationIndex2).toEqual({ test: integration3 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); @@ -456,14 +458,12 @@ describe('setupIntegration', () => { client2.captureEvent({ event_id: '2c' }); expect(integration1.preprocessEvent).toHaveBeenCalledTimes(2); - expect(integration2.preprocessEvent).toHaveBeenCalledTimes(2); + expect(integration2.preprocessEvent).toHaveBeenCalledTimes(0); expect(integration3.preprocessEvent).toHaveBeenCalledTimes(3); - expect(integration4.preprocessEvent).toHaveBeenCalledTimes(3); + expect(integration4.preprocessEvent).toHaveBeenCalledTimes(0); expect(integration1.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '1b' }, {}, client1); - expect(integration2.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '1b' }, {}, client1); expect(integration3.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '2c' }, {}, client2); - expect(integration4.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '2c' }, {}, client2); }); it('allows to mutate events in preprocessEvent', async () => { @@ -504,18 +504,20 @@ describe('setupIntegration', () => { const client1 = getTestClient(); const client2 = getTestClient(); - const integrationIndex = {}; + const integrationIndex1 = {}; + const integrationIndex2 = {}; const integration1 = new CustomIntegration(); const integration2 = new CustomIntegration(); const integration3 = new CustomIntegration(); const integration4 = new CustomIntegration(); - setupIntegration(client1, integration1, integrationIndex); - setupIntegration(client1, integration2, integrationIndex); - setupIntegration(client2, integration3, integrationIndex); - setupIntegration(client2, integration4, integrationIndex); + setupIntegration(client1, integration1, integrationIndex1); + setupIntegration(client1, integration2, integrationIndex1); + setupIntegration(client2, integration3, integrationIndex2); + setupIntegration(client2, integration4, integrationIndex2); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex1).toEqual({ test: integration1 }); + expect(integrationIndex2).toEqual({ test: integration3 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); @@ -528,30 +530,20 @@ describe('setupIntegration', () => { client2.captureEvent({ event_id: '2c' }); expect(integration1.processEvent).toHaveBeenCalledTimes(2); - expect(integration2.processEvent).toHaveBeenCalledTimes(2); + expect(integration2.processEvent).toHaveBeenCalledTimes(0); expect(integration3.processEvent).toHaveBeenCalledTimes(3); - expect(integration4.processEvent).toHaveBeenCalledTimes(3); + expect(integration4.processEvent).toHaveBeenCalledTimes(0); expect(integration1.processEvent).toHaveBeenLastCalledWith( expect.objectContaining({ event_id: '1b' }), {}, client1, ); - expect(integration2.processEvent).toHaveBeenLastCalledWith( - expect.objectContaining({ event_id: '1b' }), - {}, - client1, - ); expect(integration3.processEvent).toHaveBeenLastCalledWith( expect.objectContaining({ event_id: '2c' }), {}, client2, ); - expect(integration4.processEvent).toHaveBeenLastCalledWith( - expect.objectContaining({ event_id: '2c' }), - {}, - client2, - ); }); it('allows to mutate events in processEvent', async () => { @@ -657,7 +649,10 @@ describe('addIntegration', () => { describe('convertIntegrationFnToClass', () => { /* eslint-disable deprecation/deprecation */ it('works with a minimal integration', () => { - const integrationFn = () => ({ name: 'testName' }); + const integrationFn = () => ({ + name: 'testName', + setupOnce: () => {}, + }); const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn); @@ -671,7 +666,10 @@ describe('convertIntegrationFnToClass', () => { }); it('works with options', () => { - const integrationFn = (_options: { num: number }) => ({ name: 'testName' }); + const integrationFn = (_options: { num: number }) => ({ + name: 'testName', + setupOnce: () => {}, + }); const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn); diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts index c8df79cd22fe..b49b4b18671f 100644 --- a/packages/core/test/lib/integrations/inboundfilters.test.ts +++ b/packages/core/test/lib/integrations/inboundfilters.test.ts @@ -37,7 +37,7 @@ function createInboundFiltersEventProcessor( }), ); - client.setupIntegrations(); + client.init(); const eventProcessors = client['_eventProcessors']; const eventProcessor = eventProcessors.find(processor => processor.id === 'InboundFilters'); diff --git a/packages/core/test/lib/integrations/metadata.test.ts b/packages/core/test/lib/integrations/metadata.test.ts index 15678a66fdb6..7e8bfcea9fa4 100644 --- a/packages/core/test/lib/integrations/metadata.test.ts +++ b/packages/core/test/lib/integrations/metadata.test.ts @@ -61,6 +61,7 @@ describe('ModuleMetadata integration', () => { const client = new TestClient(options); const hub = getCurrentHub(); hub.bindClient(client); + // eslint-disable-next-line deprecation/deprecation hub.captureException(new Error('Some error')); }); }); diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index 98895ba31256..22f62bbcf89c 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -9,7 +9,7 @@ import type { ScopeContext, } from '@sentry/types'; import { GLOBAL_OBJ, createStackParser } from '@sentry/utils'; -import { getCurrentHub, getIsolationScope, setGlobalScope } from '../../src'; +import { getIsolationScope, setGlobalScope } from '../../src'; import { Scope, getGlobalScope } from '../../src/scope'; import { @@ -192,7 +192,7 @@ describe('parseEventHintOrCaptureContext', () => { describe('prepareEvent', () => { beforeEach(() => { setGlobalScope(undefined); - getCurrentHub().getIsolationScope().clear(); + getIsolationScope().clear(); }); it('works without any scope data', async () => { diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index 3122f3b3e3e5..316af3642c1c 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -1,5 +1,5 @@ -import type { Attachment, Breadcrumb, Client } from '@sentry/types'; -import { applyScopeDataToEvent } from '../../src'; +import type { Attachment, Breadcrumb, Client, Event } from '@sentry/types'; +import { applyScopeDataToEvent, getCurrentScope, getIsolationScope, withIsolationScope } from '../../src'; import { Scope, getGlobalScope, setGlobalScope } from '../../src/scope'; describe('Scope', () => { @@ -212,4 +212,294 @@ describe('Scope', () => { expect(clonedScope.getClient()).toBe(fakeClient); }); }); + + describe('.captureException()', () => { + it('should call captureException() on client with newly generated event ID if not explicitly passed in', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const exception = new Error(); + + scope.captureException(exception); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: expect.any(String) }), + scope, + ); + }); + + it('should return event ID when no client is on the scope', () => { + const scope = new Scope(); + + const exception = new Error(); + + const eventId = scope.captureException(exception); + + expect(eventId).toEqual(expect.any(String)); + }); + + it('should pass exception to captureException() on client', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const exception = new Error(); + + scope.captureException(exception); + + expect(fakeCaptureException).toHaveBeenCalledWith(exception, expect.anything(), scope); + }); + + it('should call captureException() on client with a synthetic exception', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureException(new Error()); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ syntheticException: expect.any(Error) }), + scope, + ); + }); + + it('should pass the original exception to captureException() on client', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const exception = new Error(); + scope.captureException(exception); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ originalException: exception }), + scope, + ); + }); + + it('should forward hint to captureException() on client', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureException(new Error(), { event_id: 'asdf', data: { foo: 'bar' } }); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: 'asdf', data: { foo: 'bar' } }), + scope, + ); + }); + }); + + describe('.captureMessage()', () => { + it('should call captureMessage() on client with newly generated event ID if not explicitly passed in', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('foo'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.objectContaining({ event_id: expect.any(String) }), + scope, + ); + }); + + it('should return event ID when no client is on the scope', () => { + const scope = new Scope(); + + const eventId = scope.captureMessage('foo'); + + expect(eventId).toEqual(expect.any(String)); + }); + + it('should pass exception to captureMessage() on client', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('bar'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith('bar', undefined, expect.anything(), scope); + }); + + it('should call captureMessage() on client with a synthetic exception', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('foo'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.objectContaining({ syntheticException: expect.any(Error) }), + scope, + ); + }); + + it('should pass the original exception to captureMessage() on client', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('baz'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.objectContaining({ originalException: 'baz' }), + scope, + ); + }); + + it('should forward level and hint to captureMessage() on client', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('asdf', 'fatal', { event_id: 'asdf', data: { foo: 'bar' } }); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + 'fatal', + expect.objectContaining({ event_id: 'asdf', data: { foo: 'bar' } }), + scope, + ); + }); + }); + + describe('.captureEvent()', () => { + it('should call captureEvent() on client with newly generated event ID if not explicitly passed in', () => { + const fakeCaptureEvent = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureEvent: fakeCaptureEvent, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureEvent({}); + + expect(fakeCaptureEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: expect.any(String) }), + scope, + ); + }); + + it('should return event ID when no client is on the scope', () => { + const scope = new Scope(); + + const eventId = scope.captureEvent({}); + + expect(eventId).toEqual(expect.any(String)); + }); + + it('should pass event to captureEvent() on client', () => { + const fakeCaptureEvent = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureEvent: fakeCaptureEvent, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const event: Event = { event_id: 'asdf' }; + + scope.captureEvent(event); + + expect(fakeCaptureEvent).toHaveBeenCalledWith(event, expect.anything(), scope); + }); + + it('should forward hint to captureEvent() on client', () => { + const fakeCaptureEvent = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureEvent: fakeCaptureEvent, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureEvent({}, { event_id: 'asdf', data: { foo: 'bar' } }); + + expect(fakeCaptureEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: 'asdf', data: { foo: 'bar' } }), + scope, + ); + }); + }); +}); + +describe('isolation scope', () => { + describe('withIsolationScope()', () => { + it('will pass an isolation scope without Sentry.init()', done => { + expect.assertions(1); + withIsolationScope(scope => { + expect(scope).toBeDefined(); + done(); + }); + }); + + it('will make the passed isolation scope the active isolation scope within the callback', done => { + expect.assertions(1); + withIsolationScope(scope => { + expect(getIsolationScope()).toBe(scope); + done(); + }); + }); + + it('will pass an isolation scope that is different from the current active scope', done => { + expect.assertions(1); + withIsolationScope(scope => { + expect(getCurrentScope()).not.toBe(scope); + done(); + }); + }); + + it('will always make the inner most passed scope the current scope when nesting calls', done => { + expect.assertions(1); + withIsolationScope(_scope1 => { + withIsolationScope(scope2 => { + expect(getIsolationScope()).toBe(scope2); + done(); + }); + }); + }); + }); }); diff --git a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts new file mode 100644 index 000000000000..91158d36ff01 --- /dev/null +++ b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts @@ -0,0 +1,127 @@ +import type { TransactionSource } from '@sentry/types'; +import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, makeMain } from '../../../src'; +import { Transaction, getDynamicSamplingContextFromSpan, startInactiveSpan } from '../../../src/tracing'; +import { addTracingExtensions } from '../../../src/tracing'; +import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; + +describe('getDynamicSamplingContextFromSpan', () => { + let hub: Hub; + beforeEach(() => { + const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0, release: '1.0.1' }); + const client = new TestClient(options); + hub = new Hub(client); + hub.bindClient(client); + makeMain(hub); + addTracingExtensions(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('returns the DSC provided during transaction creation', () => { + // eslint-disable-next-line deprecation/deprecation -- using old API on purpose + const transaction = new Transaction({ + name: 'tx', + metadata: { dynamicSamplingContext: { environment: 'myEnv' } }, + }); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction); + + expect(dynamicSamplingContext).toStrictEqual({ environment: 'myEnv' }); + }); + + test('returns a new DSC, if no DSC was provided during transaction creation (via attributes)', () => { + const transaction = startInactiveSpan({ name: 'tx' }); + + // Setting the attribute should overwrite the computed values + transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.56); + transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + + expect(dynamicSamplingContext).toStrictEqual({ + release: '1.0.1', + environment: 'production', + sampled: 'true', + sample_rate: '0.56', + trace_id: expect.any(String), + transaction: 'tx', + }); + }); + + test('returns a new DSC, if no DSC was provided during transaction creation (via deprecated metadata)', () => { + const transaction = startInactiveSpan({ + name: 'tx', + }); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + + expect(dynamicSamplingContext).toStrictEqual({ + release: '1.0.1', + environment: 'production', + sampled: 'true', + sample_rate: '1', + trace_id: expect.any(String), + transaction: 'tx', + }); + }); + + test('returns a new DSC, if no DSC was provided during transaction creation (via new Txn and deprecated metadata)', () => { + // eslint-disable-next-line deprecation/deprecation -- using old API on purpose + const transaction = new Transaction({ + name: 'tx', + metadata: { + sampleRate: 0.56, + source: 'route', + }, + sampled: true, + }); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + + expect(dynamicSamplingContext).toStrictEqual({ + release: '1.0.1', + environment: 'production', + sampled: 'true', + sample_rate: '0.56', + trace_id: expect.any(String), + transaction: 'tx', + }); + }); + + describe('Including transaction name in DSC', () => { + test('is not included if transaction source is url', () => { + // eslint-disable-next-line deprecation/deprecation -- using old API on purpose + const transaction = new Transaction({ + name: 'tx', + metadata: { + source: 'url', + sampleRate: 0.56, + }, + }); + + const dsc = getDynamicSamplingContextFromSpan(transaction); + expect(dsc.transaction).toBeUndefined(); + }); + + test.each([ + ['is included if transaction source is parameterized route/url', 'route'], + ['is included if transaction source is a custom name', 'custom'], + ])('%s', (_: string, source) => { + const transaction = startInactiveSpan({ + name: 'tx', + metadata: { + ...(source && { source: source as TransactionSource }), + }, + }); + + // Only setting the attribute manually because we're directly calling new Transaction() + transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + + const dsc = getDynamicSamplingContextFromSpan(transaction!); + + expect(dsc.transaction).toEqual('tx'); + }); + }); +}); diff --git a/packages/core/test/lib/tracing/span.test.ts b/packages/core/test/lib/tracing/span.test.ts index 1d36b237175a..1adff93123ac 100644 --- a/packages/core/test/lib/tracing/span.test.ts +++ b/packages/core/test/lib/tracing/span.test.ts @@ -1,58 +1,63 @@ import { timestampInSeconds } from '@sentry/utils'; import { Span } from '../../../src'; +import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; describe('span', () => { - it('works with name', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); - }); + describe('name', () => { + /* eslint-disable deprecation/deprecation */ + it('works with name', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); + }); - it('works with description', () => { - const span = new Span({ description: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); - }); + it('works with description', () => { + const span = new Span({ description: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); + }); - it('works without name', () => { - const span = new Span({}); - expect(span.name).toEqual(''); - expect(span.description).toEqual(undefined); - }); + it('works without name', () => { + const span = new Span({}); + expect(span.name).toEqual(''); + expect(span.description).toEqual(undefined); + }); - it('allows to update the name via setter', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); + it('allows to update the name via setter', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); - span.name = 'new name'; + span.name = 'new name'; - expect(span.name).toEqual('new name'); - expect(span.description).toEqual('new name'); - }); + expect(span.name).toEqual('new name'); + expect(span.description).toEqual('new name'); + }); - it('allows to update the name via setName', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); + it('allows to update the name via setName', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); - // eslint-disable-next-line deprecation/deprecation - span.setName('new name'); + // eslint-disable-next-line deprecation/deprecation + span.setName('new name'); - expect(span.name).toEqual('new name'); - expect(span.description).toEqual('new name'); - }); + expect(span.name).toEqual('new name'); + expect(span.description).toEqual('new name'); + }); - it('allows to update the name via updateName', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); + it('allows to update the name via updateName', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); - span.updateName('new name'); + span.updateName('new name'); - expect(span.name).toEqual('new name'); - expect(span.description).toEqual('new name'); + expect(span.name).toEqual('new name'); + expect(span.description).toEqual('new name'); + }); }); + /* eslint-enable deprecation/deprecation */ describe('setAttribute', () => { it('allows to set attributes', () => { @@ -69,7 +74,7 @@ describe('span', () => { span.setAttribute('boolArray', [true, false]); span.setAttribute('arrayWithUndefined', [1, undefined, 2]); - expect(span.attributes).toEqual({ + expect(span['_attributes']).toEqual({ str: 'bar', num: 1, zero: 0, @@ -87,11 +92,11 @@ describe('span', () => { span.setAttribute('str', 'bar'); - expect(Object.keys(span.attributes).length).toEqual(1); + expect(Object.keys(span['_attributes']).length).toEqual(1); span.setAttribute('str', undefined); - expect(Object.keys(span.attributes).length).toEqual(0); + expect(Object.keys(span['_attributes']).length).toEqual(0); }); it('disallows invalid attribute types', () => { @@ -112,7 +117,7 @@ describe('span', () => { it('allows to set attributes', () => { const span = new Span(); - const initialAttributes = span.attributes; + const initialAttributes = span['_attributes']; expect(initialAttributes).toEqual({}); @@ -130,7 +135,7 @@ describe('span', () => { }; span.setAttributes(newAttributes); - expect(span.attributes).toEqual({ + expect(span['_attributes']).toEqual({ str: 'bar', num: 1, zero: 0, @@ -142,14 +147,14 @@ describe('span', () => { arrayWithUndefined: [1, undefined, 2], }); - expect(span.attributes).not.toBe(newAttributes); + expect(span['_attributes']).not.toBe(newAttributes); span.setAttributes({ num: 2, numArray: [3, 4], }); - expect(span.attributes).toEqual({ + expect(span['_attributes']).toEqual({ str: 'bar', num: 2, zero: 0, @@ -167,11 +172,11 @@ describe('span', () => { span.setAttribute('str', 'bar'); - expect(Object.keys(span.attributes).length).toEqual(1); + expect(Object.keys(span['_attributes']).length).toEqual(1); span.setAttributes({ str: undefined }); - expect(Object.keys(span.attributes).length).toEqual(0); + expect(Object.keys(span['_attributes']).length).toEqual(0); }); }); @@ -226,6 +231,35 @@ describe('span', () => { }); }); + describe('spanContext', () => { + it('works with default span', () => { + const span = new Span(); + expect(span.spanContext()).toEqual({ + spanId: span['_spanId'], + traceId: span['_traceId'], + traceFlags: TRACE_FLAG_NONE, + }); + }); + + it('works sampled span', () => { + const span = new Span({ sampled: true }); + expect(span.spanContext()).toEqual({ + spanId: span['_spanId'], + traceId: span['_traceId'], + traceFlags: TRACE_FLAG_SAMPLED, + }); + }); + + it('works unsampled span', () => { + const span = new Span({ sampled: false }); + expect(span.spanContext()).toEqual({ + spanId: span['_spanId'], + traceId: span['_traceId'], + traceFlags: TRACE_FLAG_NONE, + }); + }); + }); + // Ensure that attributes & data are merged together describe('_getData', () => { it('works without data & attributes', () => { @@ -236,9 +270,11 @@ describe('span', () => { it('works with data only', () => { const span = new Span(); + // eslint-disable-next-line deprecation/deprecation span.setData('foo', 'bar'); expect(span['_getData']()).toEqual({ foo: 'bar' }); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).toBe(span.data); }); @@ -247,6 +283,7 @@ describe('span', () => { span.setAttribute('foo', 'bar'); expect(span['_getData']()).toEqual({ foo: 'bar' }); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).toBe(span.attributes); }); @@ -254,11 +291,15 @@ describe('span', () => { const span = new Span(); span.setAttribute('foo', 'foo'); span.setAttribute('bar', 'bar'); + // eslint-disable-next-line deprecation/deprecation span.setData('foo', 'foo2'); + // eslint-disable-next-line deprecation/deprecation span.setData('baz', 'baz'); expect(span['_getData']()).toEqual({ foo: 'foo', bar: 'bar', baz: 'baz' }); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).not.toBe(span.attributes); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).not.toBe(span.data); }); }); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 4659ae2e112f..c8751c90e89d 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,6 +1,13 @@ -import type { Span } from '@sentry/types'; import { Hub, addTracingExtensions, getCurrentScope, makeMain } from '../../../src'; -import { continueTrace, startInactiveSpan, startSpan, startSpanManual } from '../../../src/tracing'; +import { Scope } from '../../../src/scope'; +import { + Span, + continueTrace, + getActiveSpan, + startInactiveSpan, + startSpan, + startSpanManual, +} from '../../../src/tracing'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; beforeAll(() => { @@ -81,18 +88,6 @@ describe('startSpan', () => { expect(ref.status).toEqual(isError ? 'internal_error' : undefined); }); - it('creates & finishes span', async () => { - let _span: Span | undefined; - startSpan({ name: 'GET users/[id]' }, span => { - expect(span).toBeDefined(); - expect(span?.endTimestamp).toBeUndefined(); - _span = span; - }); - - expect(_span).toBeDefined(); - expect(_span?.endTimestamp).toBeDefined(); - }); - it('allows traceparent information to be overriden', async () => { let ref: any = undefined; client.on('finishTransaction', transaction => { @@ -160,14 +155,6 @@ describe('startSpan', () => { expect(ref.spanRecorder.spans[1].status).toEqual(isError ? 'internal_error' : undefined); }); - it('allows to pass a `startTime`', () => { - const start = startSpan({ name: 'outer', startTime: [1234, 0] }, span => { - return span?.startTimestamp; - }); - - expect(start).toEqual(1234); - }); - it('allows for span to be mutated', async () => { let ref: any = undefined; client.on('finishTransaction', transaction => { @@ -189,18 +176,58 @@ describe('startSpan', () => { expect(ref.spanRecorder.spans).toHaveLength(2); expect(ref.spanRecorder.spans[1].op).toEqual('db.query'); }); + }); - it('forks the scope', () => { - const initialScope = getCurrentScope(); + it('creates & finishes span', async () => { + let _span: Span | undefined; + startSpan({ name: 'GET users/[id]' }, span => { + expect(span).toBeDefined(); + expect(span?.endTimestamp).toBeUndefined(); + _span = span as Span; + }); - startSpan({ name: 'GET users/[id]' }, span => { - expect(getCurrentScope()).not.toBe(initialScope); - expect(getCurrentScope().getSpan()).toBe(span); - }); + expect(_span).toBeDefined(); + expect(_span?.endTimestamp).toBeDefined(); + }); + + it('allows to pass a `startTime`', () => { + const start = startSpan({ name: 'outer', startTime: [1234, 0] }, span => { + return span?.startTimestamp; + }); + + expect(start).toEqual(1234); + }); + + it('forks the scope', () => { + const initialScope = getCurrentScope(); + + startSpan({ name: 'GET users/[id]' }, span => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getActiveSpan()).toBe(span); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); + }); - expect(getCurrentScope()).toBe(initialScope); - expect(initialScope.getSpan()).toBe(undefined); + it('allows to pass a scope', () => { + const initialScope = getCurrentScope(); + + const manualScope = new Scope(); + const parentSpan = new Span({ spanId: 'parent-span-id' }); + // eslint-disable-next-line deprecation/deprecation + manualScope.setSpan(parentSpan); + + startSpan({ name: 'GET users/[id]', scope: manualScope }, span => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).toBe(manualScope); + expect(getActiveSpan()).toBe(span); + + expect(span?.parentSpanId).toBe('parent-span-id'); }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); }); }); @@ -219,16 +246,40 @@ describe('startSpanManual', () => { startSpanManual({ name: 'GET users/[id]' }, (span, finish) => { expect(getCurrentScope()).not.toBe(initialScope); - expect(getCurrentScope().getSpan()).toBe(span); + expect(getActiveSpan()).toBe(span); + + finish(); + + // Is still the active span + expect(getActiveSpan()).toBe(span); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); + }); + + it('allows to pass a scope', () => { + const initialScope = getCurrentScope(); + + const manualScope = new Scope(); + const parentSpan = new Span({ spanId: 'parent-span-id' }); + // eslint-disable-next-line deprecation/deprecation + manualScope.setSpan(parentSpan); + + startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).toBe(manualScope); + expect(getActiveSpan()).toBe(span); + expect(span?.parentSpanId).toBe('parent-span-id'); finish(); // Is still the active span - expect(getCurrentScope().getSpan()).toBe(span); + expect(getActiveSpan()).toBe(span); }); expect(getCurrentScope()).toBe(initialScope); - expect(initialScope.getSpan()).toBe(undefined); + expect(getActiveSpan()).toBe(undefined); }); it('allows to pass a `startTime`', () => { @@ -254,16 +305,31 @@ describe('startInactiveSpan', () => { }); it('does not set span on scope', () => { - const initialScope = getCurrentScope(); - const span = startInactiveSpan({ name: 'GET users/[id]' }); expect(span).toBeDefined(); - expect(initialScope.getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); + + span?.end(); + + expect(getActiveSpan()).toBeUndefined(); + }); + + it('allows to pass a scope', () => { + const manualScope = new Scope(); + const parentSpan = new Span({ spanId: 'parent-span-id' }); + // eslint-disable-next-line deprecation/deprecation + manualScope.setSpan(parentSpan); + + const span = startInactiveSpan({ name: 'GET users/[id]', scope: manualScope }); + + expect(span).toBeDefined(); + expect(span?.parentSpanId).toBe('parent-span-id'); + expect(getActiveSpan()).toBeUndefined(); span?.end(); - expect(initialScope.getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); }); it('allows to pass a `startTime`', () => { @@ -292,7 +358,7 @@ describe('continueTrace', () => { expect(result).toEqual(expectedContext); - const scope = hub.getScope(); + const scope = getCurrentScope(); expect(scope.getPropagationContext()).toEqual({ sampled: undefined, @@ -326,7 +392,7 @@ describe('continueTrace', () => { expect(result).toEqual(expectedContext); - const scope = hub.getScope(); + const scope = getCurrentScope(); expect(scope.getPropagationContext()).toEqual({ sampled: false, @@ -364,7 +430,7 @@ describe('continueTrace', () => { expect(result).toEqual(expectedContext); - const scope = hub.getScope(); + const scope = getCurrentScope(); expect(scope.getPropagationContext()).toEqual({ dsc: { @@ -406,7 +472,7 @@ describe('continueTrace', () => { expect(result).toEqual(expectedContext); - const scope = hub.getScope(); + const scope = getCurrentScope(); expect(scope.getPropagationContext()).toEqual({ dsc: { diff --git a/packages/core/test/lib/tracing/transaction.test.ts b/packages/core/test/lib/tracing/transaction.test.ts index 3be3d7dccfcc..415c0448f78e 100644 --- a/packages/core/test/lib/tracing/transaction.test.ts +++ b/packages/core/test/lib/tracing/transaction.test.ts @@ -1,44 +1,107 @@ -import { Transaction } from '../../../src'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction } from '../../../src'; describe('transaction', () => { - it('works with name', () => { - const transaction = new Transaction({ name: 'span name' }); - expect(transaction.name).toEqual('span name'); - }); + describe('name', () => { + /* eslint-disable deprecation/deprecation */ + it('works with name', () => { + const transaction = new Transaction({ name: 'span name' }); + expect(transaction.name).toEqual('span name'); + }); - it('allows to update the name via setter', () => { - const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); - expect(transaction.name).toEqual('span name'); + it('allows to update the name via setter', () => { + const transaction = new Transaction({ name: 'span name' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + expect(transaction.name).toEqual('span name'); - transaction.name = 'new name'; + transaction.name = 'new name'; - expect(transaction.name).toEqual('new name'); - expect(transaction.metadata.source).toEqual('custom'); - }); + expect(transaction.name).toEqual('new name'); + expect(transaction.metadata.source).toEqual('custom'); + }); + + it('allows to update the name via setName', () => { + const transaction = new Transaction({ name: 'span name' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + expect(transaction.name).toEqual('span name'); - it('allows to update the name via setName', () => { - const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); - expect(transaction.name).toEqual('span name'); + transaction.setName('new name'); - transaction.setMetadata({ source: 'route' }); + expect(transaction.name).toEqual('new name'); + expect(transaction.metadata.source).toEqual('custom'); + }); - // eslint-disable-next-line deprecation/deprecation - transaction.setName('new name'); + it('allows to update the name via updateName', () => { + const transaction = new Transaction({ name: 'span name' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + expect(transaction.name).toEqual('span name'); - expect(transaction.name).toEqual('new name'); - expect(transaction.metadata.source).toEqual('custom'); + transaction.updateName('new name'); + + expect(transaction.name).toEqual('new name'); + expect(transaction.metadata.source).toEqual('route'); + }); + /* eslint-enable deprecation/deprecation */ }); - it('allows to update the name via updateName', () => { - const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); - expect(transaction.name).toEqual('span name'); + describe('metadata', () => { + /* eslint-disable deprecation/deprecation */ + it('works with defaults', () => { + const transaction = new Transaction({ name: 'span name' }); + expect(transaction.metadata).toEqual({ + source: 'custom', + spanMetadata: {}, + }); + }); + + it('allows to set metadata in constructor', () => { + const transaction = new Transaction({ name: 'span name', metadata: { source: 'url', request: {} } }); + expect(transaction.metadata).toEqual({ + source: 'url', + spanMetadata: {}, + request: {}, + }); + }); + + it('allows to set source & sample rate data in constructor', () => { + const transaction = new Transaction({ + name: 'span name', + metadata: { request: {} }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 0.5, + }, + }); + expect(transaction.metadata).toEqual({ + source: 'url', + sampleRate: 0.5, + spanMetadata: {}, + request: {}, + }); + }); + + it('allows to update metadata via setMetadata', () => { + const transaction = new Transaction({ name: 'span name', metadata: { source: 'url', request: {} } }); + + transaction.setMetadata({ source: 'route' }); + + expect(transaction.metadata).toEqual({ + source: 'route', + spanMetadata: {}, + request: {}, + }); + }); + + it('allows to update metadata via setAttribute', () => { + const transaction = new Transaction({ name: 'span name', metadata: { source: 'url', request: {} } }); - transaction.updateName('new name'); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - expect(transaction.name).toEqual('new name'); - expect(transaction.metadata.source).toEqual('route'); + expect(transaction.metadata).toEqual({ + source: 'route', + spanMetadata: {}, + request: {}, + }); + }); + /* eslint-enable deprecation/deprecation */ }); }); diff --git a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts index 897524cf05e2..7543d9ed39e3 100644 --- a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts +++ b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts @@ -1,10 +1,5 @@ import type { Attachment, Breadcrumb, EventProcessor, ScopeData } from '@sentry/types'; -import { - mergeArray, - mergePropKeep, - mergePropOverwrite, - mergeScopeData, -} from '../../../src/utils/applyScopeDataToEvent'; +import { mergeAndOverwriteScopeData, mergeArray, mergeScopeData } from '../../../src/utils/applyScopeDataToEvent'; describe('mergeArray', () => { it.each([ @@ -28,55 +23,19 @@ describe('mergeArray', () => { }); }); -describe('mergePropKeep', () => { - it.each([ - [{}, {}, {}], - [{ a: 'aa' }, {}, { a: 'aa' }], - [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], - // Does not overwrite existing keys - [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'aa', b: 'bb' }], - ])('works with %s and %s', (a, b, expected) => { - const data = { tags: a } as unknown as ScopeData; - mergePropKeep(data, 'tags', b); - expect(data.tags).toEqual(expected); - }); - - it('does not deep merge', () => { - const data = { - contexts: { - app: { app_version: 'v1' }, - culture: { display_name: 'name1' }, - }, - } as unknown as ScopeData; - mergePropKeep(data, 'contexts', { - os: { name: 'os1' }, - app: { app_name: 'name1' }, - }); - expect(data.contexts).toEqual({ - os: { name: 'os1' }, - culture: { display_name: 'name1' }, - app: { app_version: 'v1' }, - }); - }); - - it('does not mutate the original object if no changes are made', () => { - const tags = { a: 'aa' }; - const data = { tags } as unknown as ScopeData; - mergePropKeep(data, 'tags', {}); - expect(data.tags).toBe(tags); - }); -}); - -describe('mergePropOverwrite', () => { +describe('mergeAndOverwriteScopeData', () => { it.each([ [{}, {}, {}], [{ a: 'aa' }, {}, { a: 'aa' }], [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], // overwrites existing keys [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'cc', b: 'bb' }], - ])('works with %s and %s', (a, b, expected) => { - const data = { tags: a } as unknown as ScopeData; - mergePropOverwrite(data, 'tags', b); + // undefined values overwrite existing values + [{ a: 'defined' }, { a: undefined, b: 'defined' }, { a: undefined, b: 'defined' }], + [{ a: 'defined' }, { a: null, b: 'defined' }, { a: null, b: 'defined' }], + ])('works with %s and %s', (oldData, newData, expected) => { + const data = { tags: oldData } as unknown as ScopeData; + mergeAndOverwriteScopeData(data, 'tags', newData); expect(data.tags).toEqual(expected); }); @@ -87,7 +46,7 @@ describe('mergePropOverwrite', () => { culture: { display_name: 'name1' }, }, } as unknown as ScopeData; - mergePropOverwrite(data, 'contexts', { + mergeAndOverwriteScopeData(data, 'contexts', { os: { name: 'os1' }, app: { app_name: 'name1' }, }); @@ -101,7 +60,7 @@ describe('mergePropOverwrite', () => { it('does not mutate the original object if no changes are made', () => { const tags = { a: 'aa' }; const data = { tags } as unknown as ScopeData; - mergePropOverwrite(data, 'tags', {}); + mergeAndOverwriteScopeData(data, 'tags', {}); expect(data.tags).toBe(tags); }); }); diff --git a/packages/core/test/lib/utils/getRootSpan.ts b/packages/core/test/lib/utils/getRootSpan.ts new file mode 100644 index 000000000000..eba622a2d884 --- /dev/null +++ b/packages/core/test/lib/utils/getRootSpan.ts @@ -0,0 +1,36 @@ +import { Span, Transaction, getRootSpan } from '../../../src'; + +describe('getRootSpan', () => { + it('returns the root span of a span (Span)', () => { + const root = new Span({ name: 'test' }); + // @ts-expect-error this is highly illegal and shouldn't happen IRL + // eslint-disable-next-line deprecation/deprecation + root.transaction = root; + + // eslint-disable-next-line deprecation/deprecation + const childSpan = root.startChild({ name: 'child' }); + expect(getRootSpan(childSpan)).toBe(root); + }); + + it('returns the root span of a span (Transaction)', () => { + // eslint-disable-next-line deprecation/deprecation + const root = new Transaction({ name: 'test' }); + + // eslint-disable-next-line deprecation/deprecation + const childSpan = root.startChild({ name: 'child' }); + expect(getRootSpan(childSpan)).toBe(root); + }); + + it('returns the span itself if it is a root span', () => { + // eslint-disable-next-line deprecation/deprecation + const span = new Transaction({ name: 'test' }); + + expect(getRootSpan(span)).toBe(span); + }); + + it('returns undefined if span has no root span', () => { + const span = new Span({ name: 'test' }); + + expect(getRootSpan(span)).toBe(undefined); + }); +}); diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index a6d84cf31166..e521df7c2dc9 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,6 +1,6 @@ import { TRACEPARENT_REGEXP, timestampInSeconds } from '@sentry/utils'; import { Span, spanToTraceHeader } from '../../../src'; -import { spanTimeInputToSeconds } from '../../../src/utils/spanUtils'; +import { spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils'; describe('spanToTraceHeader', () => { test('simple', () => { @@ -46,3 +46,84 @@ describe('spanTimeInputToSeconds', () => { expect(spanTimeInputToSeconds(timestamp)).toEqual(seconds + 0.000009); }); }); + +describe('spanToJSON', () => { + it('works with a simple span', () => { + const span = new Span(); + expect(spanToJSON(span)).toEqual({ + span_id: span.spanContext().spanId, + trace_id: span.spanContext().traceId, + origin: 'manual', + start_timestamp: span.startTimestamp, + }); + }); + + it('works with a full span', () => { + const span = new Span({ + name: 'test name', + op: 'test op', + parentSpanId: '1234', + spanId: '5678', + status: 'ok', + tags: { + foo: 'bar', + }, + traceId: 'abcd', + origin: 'auto', + startTimestamp: 123, + }); + + expect(spanToJSON(span)).toEqual({ + description: 'test name', + op: 'test op', + parent_span_id: '1234', + span_id: '5678', + status: 'ok', + tags: { + foo: 'bar', + }, + trace_id: 'abcd', + origin: 'auto', + start_timestamp: 123, + }); + }); + + it('works with a custom class without spanToJSON', () => { + const span = { + toJSON: () => { + return { + span_id: 'span_id', + trace_id: 'trace_id', + origin: 'manual', + start_timestamp: 123, + }; + }, + } as unknown as Span; + + expect(spanToJSON(span)).toEqual({ + span_id: 'span_id', + trace_id: 'trace_id', + origin: 'manual', + start_timestamp: 123, + }); + }); + + it('returns empty object if span does not have getter methods', () => { + // eslint-disable-next-line + const span = new Span().toJSON(); + + expect(spanToJSON(span as unknown as Span)).toEqual({}); + }); +}); + +describe('spanIsSampled', () => { + test('sampled', () => { + const span = new Span({ sampled: true }); + expect(spanIsSampled(span)).toBe(true); + }); + + test('not sampled', () => { + const span = new Span({ sampled: false }); + expect(spanIsSampled(span)).toBe(false); + }); +}); diff --git a/packages/core/test/mocks/integration.ts b/packages/core/test/mocks/integration.ts index 4c229ce27294..dbee06380d1c 100644 --- a/packages/core/test/mocks/integration.ts +++ b/packages/core/test/mocks/integration.ts @@ -1,6 +1,6 @@ import type { Event, EventProcessor, Integration } from '@sentry/types'; -import { getCurrentHub, getCurrentScope } from '../../src'; +import { getClient, getCurrentScope } from '../../src'; export class TestIntegration implements Integration { public static id: string = 'TestIntegration'; @@ -9,7 +9,7 @@ export class TestIntegration implements Integration { public setupOnce(): void { const eventProcessor: EventProcessor = (event: Event) => { - if (!getCurrentHub().getIntegration(TestIntegration)) { + if (!getClient()?.getIntegrationByName?.('TestIntegration')) { return event; } diff --git a/packages/deno/package.json b/packages/deno/package.json index f0957cb0a9d0..5f9b061b4375 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Deno", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno", @@ -17,10 +17,10 @@ "index.d.ts" ], "dependencies": { - "@sentry/browser": "7.92.0", - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/browser": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "devDependencies": { "@rollup/plugin-typescript": "^11.1.5", diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index f70d511ae8a3..cf216aa033cb 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -38,6 +38,7 @@ export { extractTraceparentData, continueTrace, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -64,6 +65,7 @@ export { // eslint-disable-next-line deprecation/deprecation trace, withScope, + withIsolationScope, captureCheckIn, withMonitor, setMeasurement, diff --git a/packages/deno/src/integrations/context.ts b/packages/deno/src/integrations/context.ts index e54f4ec21a87..426d296f6efc 100644 --- a/packages/deno/src/integrations/context.ts +++ b/packages/deno/src/integrations/context.ts @@ -55,6 +55,8 @@ async function addDenoRuntimeContext(event: Event): Promise { const denoContextIntegration: IntegrationFn = () => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event) { return addDenoRuntimeContext(event); }, diff --git a/packages/deno/src/integrations/contextlines.ts b/packages/deno/src/integrations/contextlines.ts index 38fe0efd3433..f3099151908a 100644 --- a/packages/deno/src/integrations/contextlines.ts +++ b/packages/deno/src/integrations/contextlines.ts @@ -52,6 +52,8 @@ const denoContextLinesIntegration: IntegrationFn = (options: ContextLinesOptions return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event) { return addSourceContext(event, contextLines); }, diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts index 06194037b6d1..05b46b58c7a9 100644 --- a/packages/deno/src/integrations/globalhandlers.ts +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -22,6 +22,8 @@ const globalHandlersIntegration: IntegrationFn = (options?: GlobalHandlersIntegr return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client) { if (_options.error) { installGlobalErrorHandler(client); diff --git a/packages/deno/src/integrations/normalizepaths.ts b/packages/deno/src/integrations/normalizepaths.ts index a8143c8b078b..39a3bab5a1c5 100644 --- a/packages/deno/src/integrations/normalizepaths.ts +++ b/packages/deno/src/integrations/normalizepaths.ts @@ -69,6 +69,8 @@ const normalizePathsIntegration: IntegrationFn = () => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event) { // This error.stack hopefully contains paths that traverse the app cwd const error = new Error(); diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index f278e370312b..607d87b968bc 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -77,8 +77,8 @@ snapshot[`captureException 1`] = ` lineno: 526, }, { - colno: 24, - context_line: " hub.captureException(something());", + colno: 27, + context_line: " client.captureException(something());", filename: "app:///test/mod.test.ts", function: "", in_app: true, @@ -112,7 +112,7 @@ snapshot[`captureException 1`] = ` post_context: [ " }", "", - " hub.captureException(something());", + " client.captureException(something());", "", " await delay(200);", " await assertSnapshot(t, ev);", @@ -121,7 +121,7 @@ snapshot[`captureException 1`] = ` pre_context: [ "Deno.test('captureException', async t => {", " let ev: sentryTypes.Event | undefined;", - " const [hub] = getTestClient(event => {", + " const [, client] = getTestClient(event => {", " ev = event;", " });", "", diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts index a14e04b61ff9..1568584d8281 100644 --- a/packages/deno/test/mod.test.ts +++ b/packages/deno/test/mod.test.ts @@ -35,7 +35,7 @@ function delay(time: number): Promise { Deno.test('captureException', async t => { let ev: sentryTypes.Event | undefined; - const [hub] = getTestClient(event => { + const [, client] = getTestClient(event => { ev = event; }); @@ -43,7 +43,7 @@ Deno.test('captureException', async t => { return new Error('Some unhandled error'); } - hub.captureException(something()); + client.captureException(something()); await delay(200); await assertSnapshot(t, ev); @@ -51,11 +51,11 @@ Deno.test('captureException', async t => { Deno.test('captureMessage', async t => { let ev: sentryTypes.Event | undefined; - const [hub] = getTestClient(event => { + const [, client] = getTestClient(event => { ev = event; }); - hub.captureMessage('Some error message'); + client.captureMessage('Some error message'); await delay(200); await assertSnapshot(t, ev); diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 2a5da643c984..009ab2dfe6e0 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -2,13 +2,14 @@ import { assert, warn } from '@ember/debug'; import type Route from '@ember/routing/route'; import { next } from '@ember/runloop'; import { getOwnConfig, isDevelopingApp, macroCondition } from '@embroider/macros'; +import { startSpan } from '@sentry/browser'; import type { BrowserOptions } from '@sentry/browser'; import * as Sentry from '@sentry/browser'; import { SDK_VERSION } from '@sentry/browser'; -import type { Transaction } from '@sentry/types'; -import { GLOBAL_OBJ, timestampInSeconds } from '@sentry/utils'; +import { GLOBAL_OBJ } from '@sentry/utils'; import Ember from 'ember'; +import type { Transaction } from '@sentry/types'; import type { EmberSentryConfig, GlobalConfig, OwnConfig } from './types'; function _getSentryInitConfig(): EmberSentryConfig['sentry'] { @@ -66,8 +67,14 @@ export function InitSentryForEmber(_runtimeConfig?: BrowserOptions): void { } } +/** + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. + */ export const getActiveTransaction = (): Transaction | undefined => { - return Sentry.getCurrentHub().getScope().getTransaction(); + // eslint-disable-next-line deprecation/deprecation + return Sentry.getCurrentScope().getTransaction(); }; type RouteConstructor = new (...args: ConstructorParameters) => Route; @@ -80,22 +87,16 @@ export const instrumentRoutePerformance = (BaseRoute fn: X, args: Parameters, ): Promise> => { - const startTimestamp = timestampInSeconds(); - const result = await fn(...args); - - const currentTransaction = getActiveTransaction(); - if (!currentTransaction) { - return result; - } - currentTransaction - .startChild({ + return startSpan( + { op, - description, + name: description, origin: 'auto.ui.ember', - startTimestamp, - }) - .end(); - return result; + }, + () => { + return fn(...args); + }, + ); }; const routeName = BaseRoute.name; diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index 41d10842fa7c..b25125b28da6 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -12,7 +12,7 @@ import type { Span, Transaction } from '@sentry/types'; import { GLOBAL_OBJ, browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/utils'; import type { BrowserClient } from '..'; -import { getActiveTransaction } from '..'; +import { getActiveSpan, startInactiveSpan } from '..'; import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig, StartTransactionFunction } from '../types'; type SentryTestRouterService = RouterService & { @@ -149,9 +149,9 @@ export function _instrumentEmberRouter( 'routing.instrumentation': '@sentry/ember', }, }); - transitionSpan = activeTransaction?.startChild({ + transitionSpan = startInactiveSpan({ op: 'ui.ember.transition', - description: `route:${fromRoute} -> route:${toRoute}`, + name: `route:${fromRoute} -> route:${toRoute}`, origin: 'auto.ui.ember', }); }); @@ -195,8 +195,8 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { if (previousInstance) { return; } - const activeTransaction = getActiveTransaction(); - if (!activeTransaction) { + const activeSpan = getActiveSpan(); + if (!activeSpan) { return; } if (currentQueueSpan) { @@ -211,22 +211,20 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { const minQueueDuration = minimumRunloopQueueDuration ?? 5; if ((now - currentQueueStart) * 1000 >= minQueueDuration) { - activeTransaction - ?.startChild({ - op: `ui.ember.runloop.${queue}`, - origin: 'auto.ui.ember', - startTimestamp: currentQueueStart, - endTimestamp: now, - }) - .end(); + startInactiveSpan({ + name: 'runloop', + op: `ui.ember.runloop.${queue}`, + origin: 'auto.ui.ember', + startTimestamp: currentQueueStart, + })?.end(now); } currentQueueStart = undefined; } // Setup for next queue - const stillActiveTransaction = getActiveTransaction(); - if (!stillActiveTransaction) { + const stillActiveSpan = getActiveSpan(); + if (!stillActiveSpan) { return; } currentQueueStart = timestampInSeconds(); @@ -286,15 +284,12 @@ function processComponentRenderAfter( const componentRenderDuration = now - begin.now; if (componentRenderDuration * 1000 >= minComponentDuration) { - const activeTransaction = getActiveTransaction(); - - activeTransaction?.startChild({ + startInactiveSpan({ + name: payload.containerKey || payload.object, op, - description: payload.containerKey || payload.object, origin: 'auto.ui.ember', startTimestamp: begin.now, - endTimestamp: now, - }); + })?.end(now); } } @@ -372,13 +367,12 @@ function _instrumentInitialLoad(config: EmberSentryConfig): void { const startTimestamp = (measure.startTime + browserPerformanceTimeOrigin) / 1000; const endTimestamp = startTimestamp + measure.duration / 1000; - const transaction = getActiveTransaction(); - const span = transaction?.startChild({ + startInactiveSpan({ op: 'ui.ember.init', + name: 'init', origin: 'auto.ui.ember', startTimestamp, - }); - span?.end(endTimestamp); + })?.end(endTimestamp); performance.clearMarks(startName); performance.clearMarks(endName); @@ -443,19 +437,19 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance) }); if (macroCondition(isTesting())) { - const client = Sentry.getCurrentHub().getClient(); + const client = Sentry.getClient(); if ( client && - (client as BrowserClient).getIntegrationById && - (client as BrowserClient).getIntegrationById('BrowserTracing') + (client as BrowserClient).getIntegrationByName && + (client as BrowserClient).getIntegrationByName('BrowserTracing') ) { // Initializers are called more than once in tests, causing the integrations to not be setup correctly. return; } } - const client = Sentry.getCurrentHub().getClient(); + const client = Sentry.getClient(); if (client && client.addIntegration) { client.addIntegration(browserTracing); } diff --git a/packages/ember/package.json b/packages/ember/package.json index d99c0a176270..3f5459f55e57 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -32,9 +32,10 @@ }, "dependencies": { "@embroider/macros": "^1.9.0", - "@sentry/browser": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry/browser": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "ember-auto-import": "^1.12.1 || ^2.4.3", "ember-cli-babel": "^7.26.11", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/ember/tests/acceptance/sentry-replay-test.ts b/packages/ember/tests/acceptance/sentry-replay-test.ts index 1614dc22817a..4e541c00a1df 100644 --- a/packages/ember/tests/acceptance/sentry-replay-test.ts +++ b/packages/ember/tests/acceptance/sentry-replay-test.ts @@ -1,6 +1,6 @@ import { visit } from '@ember/test-helpers'; import * as Sentry from '@sentry/ember'; -import type { ReplayContainer } from '@sentry/replay/build/npm/types/types'; +import type { BrowserClient, Replay } from '@sentry/ember'; import { setupApplicationTest } from 'ember-qunit'; import { module, test } from 'qunit'; @@ -13,10 +13,10 @@ module('Acceptance | Sentry Session Replay', function (hooks) { test('Test replay', async function (assert) { await visit('/replay'); - const integration = Sentry.getCurrentHub().getIntegration(Sentry.Replay); + const integration = Sentry.getClient()?.getIntegrationByName('Replay'); assert.ok(integration); - const replay = (integration as Sentry.Replay)['_replay'] as ReplayContainer; + const replay = (integration as Sentry.Replay)['_replay'] as Replay['_replay']; assert.true(replay.isEnabled()); assert.false(replay.isPaused()); diff --git a/packages/ember/tests/dummy/app/routes/replay.ts b/packages/ember/tests/dummy/app/routes/replay.ts index 9702fe8aa1af..ce9ebe2fa813 100644 --- a/packages/ember/tests/dummy/app/routes/replay.ts +++ b/packages/ember/tests/dummy/app/routes/replay.ts @@ -5,9 +5,8 @@ import * as Sentry from '@sentry/ember'; export default class ReplayRoute extends Route { public async beforeModel(): Promise { const { Replay } = Sentry; - - if (!Sentry.getCurrentHub().getIntegration(Replay)) { - const client = Sentry.getCurrentHub().getClient() as BrowserClient; + const client = Sentry.getClient(); + if (client && !client.getIntegrationByName('Replay')) { client.addIntegration(new Replay()); } } diff --git a/packages/ember/tests/helpers/utils.ts b/packages/ember/tests/helpers/utils.ts index 3ec336cfa59c..99109074219e 100644 --- a/packages/ember/tests/helpers/utils.ts +++ b/packages/ember/tests/helpers/utils.ts @@ -1,3 +1,4 @@ +import { spanToJSON } from '@sentry/core'; import type { Event } from '@sentry/types'; const defaultAssertOptions = { @@ -67,7 +68,7 @@ export function assertSentryTransactions( const filteredSpans = spans .filter(span => !span.op?.startsWith('ui.ember.runloop.')) .map(s => { - return `${s.op} | ${s.description}`; + return `${s.op} | ${spanToJSON(s).description}`; }); assert.true( diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index 9dc3f141399a..021263d31d5c 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -22,8 +22,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "7.92.0", - "@sentry-internal/typescript": "7.92.0", + "@sentry-internal/eslint-plugin-sdk": "7.93.0", + "@sentry-internal/typescript": "7.93.0", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 9833b053d124..93c98c13ac41 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/feedback/README.md b/packages/feedback/README.md index 673bf344ea75..37b0cd017e63 100644 --- a/packages/feedback/README.md +++ b/packages/feedback/README.md @@ -212,7 +212,7 @@ import {Feedback} from '@sentry-internal/feedback'; function MyFeedbackButton() { const client = getCurrentHub().getClient(); - const feedback = client?.getIntegration(Feedback); + const feedback = client?.getIntegrationByName('Feedback'); // Don't render custom feedback button if Feedback integration not installed if (!feedback) { @@ -258,7 +258,7 @@ import {BrowserClient, getCurrentHub} from '@sentry/react'; import {Feedback} from '@sentry-internal/feedback'; document.getElementById('my-feedback-form').addEventListener('submit', (event) => { - const feedback = getCurrentHub().getClient()?.getIntegration(Feedback); + const feedback = getCurrentHub().getClient()?.getIntegrationByName('Feedback'); const formData = new FormData(event.currentTarget); feedback.sendFeedback(formData); event.preventDefault(); diff --git a/packages/feedback/package.json b/packages/feedback/package.json index d74d876d3764..b150bc4963d0 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "7.92.0", + "version": "7.93.0", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", @@ -29,9 +29,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "scripts": { "build": "run-p build:transpile build:types build:bundle", diff --git a/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts b/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts index 46311f1f0374..21e8326cec2d 100644 --- a/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts +++ b/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts @@ -1,4 +1,5 @@ import type { Hub, Scope } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { getCurrentHub } from '@sentry/core'; import type { Client, FeedbackEvent } from '@sentry/types'; @@ -14,9 +15,7 @@ describe('Unit | util | prepareFeedbackEvent', () => { hub = getCurrentHub(); client = new TestClient(getDefaultClientOptions()); hub.bindClient(client); - - client = hub.getClient()!; - scope = hub.getScope(); + scope = getCurrentScope(); }); afterEach(() => { diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index f43952c67790..65dcf6901ed2 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -37,10 +37,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.92.0", - "@sentry/react": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry/core": "7.93.0", + "@sentry/react": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "@sentry/webpack-plugin": "1.19.0" }, "peerDependencies": { diff --git a/packages/hub/package.json b/packages/hub/package.json index 1c6d5acdaf63..cefe680a3799 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/hub", - "version": "7.92.0", + "version": "7.93.0", "description": "Sentry hub which handles global state managment.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/hub", @@ -29,9 +29,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/hub/test/hub.test.ts b/packages/hub/test/hub.test.ts index fbdc3993b989..08ec6a22130a 100644 --- a/packages/hub/test/hub.test.ts +++ b/packages/hub/test/hub.test.ts @@ -3,6 +3,7 @@ import type { Client, Event, EventType } from '@sentry/types'; +import { getCurrentScope, makeMain } from '@sentry/core'; import { Hub, Scope, getCurrentHub } from '../src'; const clientFn: any = jest.fn(); @@ -18,6 +19,7 @@ function makeClient() { getIntegration: jest.fn(), setupIntegrations: jest.fn(), captureMessage: jest.fn(), + captureSession: jest.fn(), } as unknown as Client; } @@ -453,4 +455,102 @@ describe('Hub', () => { expect(hub.shouldSendDefaultPii()).toBe(true); }); }); + + describe('session APIs', () => { + beforeEach(() => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + }); + + describe('startSession', () => { + it('starts a session', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + const session = hub.startSession(); + + expect(session).toMatchObject({ + status: 'ok', + errors: 0, + init: true, + environment: 'production', + ignoreDuration: false, + sid: expect.any(String), + did: undefined, + timestamp: expect.any(Number), + started: expect.any(Number), + duration: expect.any(Number), + toJSON: expect.any(Function), + }); + }); + + it('ends a previously active session and removes it from the scope', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session1 = hub.startSession(); + + expect(session1.status).toBe('ok'); + expect(getCurrentScope().getSession()).toBe(session1); + + const session2 = hub.startSession(); + + expect(session2.status).toBe('ok'); + expect(session1.status).toBe('exited'); + expect(getCurrentHub().getScope().getSession()).toBe(session2); + }); + }); + + describe('endSession', () => { + it('ends a session and removes it from the scope', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session = hub.startSession(); + + expect(session.status).toBe('ok'); + expect(getCurrentScope().getSession()).toBe(session); + + hub.endSession(); + + expect(session.status).toBe('exited'); + expect(getCurrentHub().getScope().getSession()).toBe(undefined); + }); + }); + + describe('captureSession', () => { + it('captures a session without ending it by default', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session = hub.startSession(); + + expect(session.status).toBe('ok'); + expect(getCurrentScope().getSession()).toBe(session); + + hub.captureSession(); + + expect(testClient.captureSession).toHaveBeenCalledWith(expect.objectContaining({ status: 'ok' })); + }); + + it('captures a session and ends it if end is `true`', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session = hub.startSession(); + + expect(session.status).toBe('ok'); + expect(hub.getScope().getSession()).toBe(session); + + hub.captureSession(true); + + expect(testClient.captureSession).toHaveBeenCalledWith(expect.objectContaining({ status: 'exited' })); + }); + }); + }); }); diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index 4cb8694b9cca..b8cfaf1914e0 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -361,6 +361,7 @@ describe('Scope', () => { const scope = new Scope(); const span = { fake: 'span', + spanContext: () => ({}), toJSON: () => ({ origin: 'manual' }), } as any; scope.setSpan(span); @@ -374,6 +375,7 @@ describe('Scope', () => { const scope = new Scope(); const span = { fake: 'span', + spanContext: () => ({}), toJSON: () => ({ a: 'b' }), } as any; scope.setSpan(span); @@ -392,8 +394,8 @@ describe('Scope', () => { const scope = new Scope(); const transaction = { fake: 'span', - toJSON: () => ({ a: 'b' }), - name: 'fake transaction', + spanContext: () => ({}), + toJSON: () => ({ a: 'b', description: 'fake transaction' }), getDynamicSamplingContext: () => ({}), } as any; transaction.transaction = transaction; // because this is a transaction, its `transaction` pointer points to itself @@ -407,9 +409,14 @@ describe('Scope', () => { test('adds `transaction` tag when span on scope', async () => { expect.assertions(1); const scope = new Scope(); - const transaction = { name: 'fake transaction', getDynamicSamplingContext: () => ({}) }; + const transaction = { + spanContext: () => ({}), + toJSON: () => ({ description: 'fake transaction' }), + getDynamicSamplingContext: () => ({}), + }; const span = { fake: 'span', + spanContext: () => ({}), toJSON: () => ({ a: 'b' }), transaction, } as any; diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index d589e9d254b8..14b58b630756 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "7.92.0", + "version": "7.93.0", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -39,8 +39,8 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "engines": { "node": ">=12" diff --git a/packages/integrations/package.json b/packages/integrations/package.json index d14b0b1b5f23..139f6eebb123 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/integrations", - "version": "7.92.0", + "version": "7.93.0", "description": "Pluggable integrations that can be used to enhance JS SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/integrations", @@ -29,13 +29,13 @@ } }, "dependencies": { - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "localforage": "^1.8.1" }, "devDependencies": { - "@sentry/browser": "7.92.0", + "@sentry/browser": "7.93.0", "chai": "^4.1.2" }, "scripts": { diff --git a/packages/integrations/src/captureconsole.ts b/packages/integrations/src/captureconsole.ts index 4f37ecb1011a..4d9aa04ce116 100644 --- a/packages/integrations/src/captureconsole.ts +++ b/packages/integrations/src/captureconsole.ts @@ -20,6 +20,8 @@ const captureConsoleIntegration = ((options: CaptureConsoleOptions = {}) => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client) { if (!('console' in GLOBAL_OBJ)) { return; diff --git a/packages/integrations/src/contextlines.ts b/packages/integrations/src/contextlines.ts index 656080ec3182..143fb0232bec 100644 --- a/packages/integrations/src/contextlines.ts +++ b/packages/integrations/src/contextlines.ts @@ -23,6 +23,8 @@ const contextLinesIntegration: IntegrationFn = (options: ContextLinesOptions = { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event) { return addSourceContext(event, contextLines); }, diff --git a/packages/integrations/src/debug.ts b/packages/integrations/src/debug.ts index 6edb9939269a..8e20ce7f9dfc 100644 --- a/packages/integrations/src/debug.ts +++ b/packages/integrations/src/debug.ts @@ -20,6 +20,8 @@ const debugIntegration = ((options: DebugOptions = {}) => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client) { if (!client.on) { return; diff --git a/packages/integrations/src/dedupe.ts b/packages/integrations/src/dedupe.ts index 1e3ae1be7626..97a47193b19a 100644 --- a/packages/integrations/src/dedupe.ts +++ b/packages/integrations/src/dedupe.ts @@ -11,6 +11,8 @@ const dedupeIntegration = (() => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(currentEvent) { // We want to ignore any non-error type events, e.g. transactions or replays // These should never be deduped, and also not be compared against as _previousEvent. diff --git a/packages/integrations/src/extraerrordata.ts b/packages/integrations/src/extraerrordata.ts index 8d9d72cb81d7..6b340a1283bc 100644 --- a/packages/integrations/src/extraerrordata.ts +++ b/packages/integrations/src/extraerrordata.ts @@ -28,6 +28,8 @@ const extraErrorDataIntegration = ((options: Partial = {} return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event, hint) { return _enhanceEventWithErrorData(event, hint, depth, captureErrorCause); }, diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts index 74142487473a..0ef2dcca3f27 100644 --- a/packages/integrations/src/httpclient.ts +++ b/packages/integrations/src/httpclient.ts @@ -47,6 +47,8 @@ const httpClientIntegration = ((options: Partial = {}) => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client): void { _wrapFetch(client, _options); _wrapXHR(client, _options); diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index b7dcc1f4716a..2b7b986345ec 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -7,6 +7,7 @@ export { Offline } from './offline'; export { ReportingObserver } from './reportingobserver'; export { RewriteFrames } from './rewriteframes'; export { SessionTiming } from './sessiontiming'; +// eslint-disable-next-line deprecation/deprecation export { Transaction } from './transaction'; export { HttpClient } from './httpclient'; export { ContextLines } from './contextlines'; diff --git a/packages/integrations/src/offline.ts b/packages/integrations/src/offline.ts index 9708ffbdc050..67a9df76eca7 100644 --- a/packages/integrations/src/offline.ts +++ b/packages/integrations/src/offline.ts @@ -81,6 +81,7 @@ export class Offline implements Integration { } const eventProcessor: EventProcessor = event => { + // eslint-disable-next-line deprecation/deprecation if (this.hub && this.hub.getIntegration(Offline)) { // cache if we are positively offline if ('navigator' in WINDOW && 'onLine' in WINDOW.navigator && !WINDOW.navigator.onLine) { diff --git a/packages/integrations/src/rewriteframes.ts b/packages/integrations/src/rewriteframes.ts index d82bde5728c6..7bb5957607b2 100644 --- a/packages/integrations/src/rewriteframes.ts +++ b/packages/integrations/src/rewriteframes.ts @@ -69,6 +69,8 @@ const rewriteFramesIntegration = ((options: RewriteFramesOptions = {}) => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(originalEvent) { let processedEvent = originalEvent; diff --git a/packages/integrations/src/sessiontiming.ts b/packages/integrations/src/sessiontiming.ts index 4398d170a981..b3511e6c3527 100644 --- a/packages/integrations/src/sessiontiming.ts +++ b/packages/integrations/src/sessiontiming.ts @@ -8,6 +8,8 @@ const sessionTimingIntegration = (() => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event) { const now = Date.now(); diff --git a/packages/integrations/src/transaction.ts b/packages/integrations/src/transaction.ts index c44c94c7fe06..ce1701f041a4 100644 --- a/packages/integrations/src/transaction.ts +++ b/packages/integrations/src/transaction.ts @@ -6,6 +6,8 @@ const INTEGRATION_NAME = 'Transaction'; const transactionIntegration = (() => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event) { const frames = _getFramesFromEvent(event); @@ -24,7 +26,10 @@ const transactionIntegration = (() => { }; }) satisfies IntegrationFn; -/** Add node transaction to the event */ +/** + * Add node transaction to the event. + * @deprecated This integration will be removed in v8. + */ // eslint-disable-next-line deprecation/deprecation export const Transaction = convertIntegrationFnToClass(INTEGRATION_NAME, transactionIntegration); diff --git a/packages/integrations/test/transaction.test.ts b/packages/integrations/test/transaction.test.ts index bfc6096a519e..617af17c171a 100644 --- a/packages/integrations/test/transaction.test.ts +++ b/packages/integrations/test/transaction.test.ts @@ -1,5 +1,6 @@ import { Transaction } from '../src/transaction'; +// eslint-disable-next-line deprecation/deprecation const transaction = new Transaction(); describe('Transaction', () => { diff --git a/packages/nextjs/.eslintrc.js b/packages/nextjs/.eslintrc.js index dfe7a92443f9..885839f52e18 100644 --- a/packages/nextjs/.eslintrc.js +++ b/packages/nextjs/.eslintrc.js @@ -19,19 +19,5 @@ module.exports = { project: ['../../tsconfig.dev.json'], }, }, - { - files: ['test/buildProcess/**'], - parserOptions: { - sourceType: 'module', - }, - plugins: ['react'], - extends: ['../../.eslintrc.js', 'plugin:react/recommended'], - rules: { - // Prop types validation is not useful in test environments - 'react/prop-types': 'off', - // Nextjs takes care of including react, so we don't explicitly need to - 'react/react-in-jsx-scope': 'off', - }, - }, ], }; diff --git a/packages/nextjs/jest.config.js b/packages/nextjs/jest.config.js index 4001570bcdb9..edaa219fa7be 100644 --- a/packages/nextjs/jest.config.js +++ b/packages/nextjs/jest.config.js @@ -2,7 +2,5 @@ const baseConfig = require('../../jest/jest.config.js'); module.exports = { ...baseConfig, - // This prevents the build tests from running when unit tests run. (If they do, they fail, because the build being - // tested hasn't necessarily run yet.) - testPathIgnorePatterns: ['/test/buildProcess/', '/test/integration/'], + testPathIgnorePatterns: ['/test/integration/'], }; diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 89ebd5d64aeb..0921a7d19007 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -25,13 +25,13 @@ }, "dependencies": { "@rollup/plugin-commonjs": "24.0.0", - "@sentry/core": "7.92.0", - "@sentry/integrations": "7.92.0", - "@sentry/node": "7.92.0", - "@sentry/react": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", - "@sentry/vercel-edge": "7.92.0", + "@sentry/core": "7.93.0", + "@sentry/integrations": "7.93.0", + "@sentry/node": "7.93.0", + "@sentry/react": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", + "@sentry/vercel-edge": "7.93.0", "@sentry/webpack-plugin": "1.21.0", "chalk": "3.0.0", "resolve": "1.22.8", @@ -71,8 +71,7 @@ "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", "test": "yarn test:unit", - "test:all": "run-s test:unit test:integration test:build", - "test:build": "yarn ts-node test/buildProcess/runTest.ts", + "test:all": "run-s test:unit test:integration", "test:unit": "jest", "test:integration": "./test/run-integration-tests.sh && yarn test:types", "test:integration:clean": "(cd test/integration && rimraf .cache node_modules build)", diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 1e31ffaaef0c..5f2064c690e4 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -186,6 +186,7 @@ export function pagesRouterInstrumentation( // We don't want to finish the navigation transaction on `routeChangeComplete`, since users might want to attach // spans to that transaction even after `routeChangeComplete` is fired (eg. HTTP requests in some useEffect // hooks). Instead, we'll simply let the navigation transaction finish itself (it's an `IdleTransaction`). + // eslint-disable-next-line deprecation/deprecation const nextRouteChangeSpan = navigationTransaction.startChild({ op: 'ui.nextjs.route-change', origin: 'auto.ui.nextjs.pages_router_instrumentation', diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 9c479a88ceeb..59114ddee709 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -1,4 +1,11 @@ -import { addTracingExtensions, captureException, continueTrace, handleCallbackErrors, startSpan } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + addTracingExtensions, + captureException, + continueTrace, + handleCallbackErrors, + startSpan, +} from '@sentry/core'; import { winterCGRequestToRequestData } from '@sentry/utils'; import type { EdgeRouteHandler } from '../../edge/types'; @@ -34,10 +41,11 @@ export function withEdgeWrapping( name: options.spanDescription, op: options.spanOp, origin: 'auto.function.nextjs.withEdgeWrapping', + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, request: req instanceof Request ? winterCGRequestToRequestData(req) : undefined, - source: 'route', }, }, async span => { diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index da0c11df8640..f7e0917f2c39 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -1,6 +1,8 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, + getActiveSpan, getActiveTransaction, getCurrentScope, runWithAsyncContext, @@ -85,7 +87,7 @@ export function withTracedServerSideDataFetcher Pr return async function (this: unknown, ...args: Parameters): Promise> { return runWithAsyncContext(async () => { const scope = getCurrentScope(); - const previousSpan: Span | undefined = getTransactionFromRequest(req) ?? scope.getSpan(); + const previousSpan: Span | undefined = getTransactionFromRequest(req) ?? getActiveSpan(); let dataFetcherSpan; const sentryTrace = @@ -131,6 +133,7 @@ export function withTracedServerSideDataFetcher Pr spanToContinue = previousSpan; } + // eslint-disable-next-line deprecation/deprecation dataFetcherSpan = spanToContinue.startChild({ op: 'function.nextjs', description: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, @@ -154,6 +157,7 @@ export function withTracedServerSideDataFetcher Pr }); } + // eslint-disable-next-line deprecation/deprecation scope.setSpan(dataFetcherSpan); scope.setSDKProcessingMetadata({ request: req }); @@ -167,6 +171,7 @@ export function withTracedServerSideDataFetcher Pr throw e; } finally { dataFetcherSpan.end(); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(previousSpan); if (!platformSupportsStreaming()) { await flushQueue(); @@ -193,6 +198,7 @@ export async function callDataFetcherTraced Promis ): Promise> { const { parameterizedRoute, dataFetchingMethodName } = options; + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (!transaction) { @@ -204,11 +210,12 @@ export async function callDataFetcherTraced Promis // right here so making that check will probabably not even be necessary. // Logic will be: If there is no active transaction, start one with correct name and source. If there is an active // transaction, create a child span with correct name and source. - transaction.name = parameterizedRoute; - transaction.metadata.source = 'route'; + transaction.updateName(parameterizedRoute); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); // Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another // route's transaction + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ op: 'function.nextjs', origin: 'auto.function.nextjs', diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 85872ac7d703..01e1c75d6f3f 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -104,19 +104,16 @@ async function withServerActionInstrumentationImplementation = {}; options.formData.forEach((value, key) => { - if (typeof value === 'string') { - formDataObject[key] = value; - } else { - formDataObject[key] = '[non-string value]'; - } + span?.setAttribute( + `server_action_form_data.${key}`, + typeof value === 'string' ? value : '[non-string value]', + ); }); - span?.setData('server_action_form_data', formDataObject); } return result; diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index fc8f602f524b..16228aa0cda8 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, continueTrace, @@ -108,9 +109,12 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri name: `${reqMethod}${reqPath}`, op: 'http.server', origin: 'auto.http.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, - source: 'route', request: req, }, }, diff --git a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts index cd6cc4934493..df18b2ad952d 100644 --- a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type App from 'next/app'; @@ -52,6 +58,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI }; } = await tracedGetInitialProps.apply(thisArg, args); + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call @@ -65,7 +72,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI if (requestTransaction) { appGetInitialProps.pageProps._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); appGetInitialProps.pageProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts index b26e4a2434c3..44a171d8e6d5 100644 --- a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPageContext } from 'next'; import type { ErrorProps } from 'next/error'; @@ -53,11 +59,12 @@ export function wrapErrorGetInitialPropsWithSentry( _sentryBaggage?: string; } = await tracedGetInitialProps.apply(thisArg, args); + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { errorGetInitialProps._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); errorGetInitialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index fe90b6f6ca39..f2e829704dd6 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, continueTrace, @@ -69,9 +70,12 @@ export function wrapGenerationFunctionWithSentry a origin: 'auto.function.nextjs', ...transactionContext, data, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, - source: 'url', request: { headers: headers ? winterCGHeadersToDict(headers) : undefined, }, diff --git a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts index df4e3febfefc..1a6743765cd6 100644 --- a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPage } from 'next'; @@ -49,11 +55,12 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro _sentryBaggage?: string; } = (await tracedGetInitialProps.apply(thisArg, args)) ?? {}; // Next.js allows undefined to be returned from a getInitialPropsFunction. + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { initialProps._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); initialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts index c74f9db7292b..691570f87683 100644 --- a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { GetServerSideProps } from 'next'; @@ -46,11 +52,12 @@ export function wrapGetServerSidePropsWithSentry( >); if (serverSideProps && 'props' in serverSideProps) { + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { serverSideProps.props._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); serverSideProps.props._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } } diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 427879b3e843..a0a1ae2f77aa 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, continueTrace, @@ -61,12 +62,15 @@ export function wrapServerComponentWithSentry any> name: `${componentType} Server Component (${componentRoute})`, status: 'ok', origin: 'auto.function.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, request: { headers: completeHeadersDict, }, - source: 'component', }, }, span => { diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index dabba7741e01..71e3072d68b5 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -1,4 +1,4 @@ -import { getCurrentScope } from '@sentry/core'; +import { getActiveSpan } from '@sentry/core'; import { withEdgeWrapping } from '../common/utils/edgeWrapperUtils'; import type { EdgeRouteHandler } from './types'; @@ -14,7 +14,7 @@ export function wrapApiHandlerWithSentry( apply: (wrappingTarget, thisArg, args: Parameters) => { const req = args[0]; - const activeSpan = getCurrentScope().getSpan(); + const activeSpan = getActiveSpan(); const wrappedHandler = withEdgeWrapping(wrappingTarget, { spanDescription: diff --git a/packages/nextjs/test/buildProcess/jest.config.js b/packages/nextjs/test/buildProcess/jest.config.js deleted file mode 100644 index 2c7f321ad9ee..000000000000 --- a/packages/nextjs/test/buildProcess/jest.config.js +++ /dev/null @@ -1,4 +0,0 @@ -// In order to not have the build tests run as part of the unit test suite, we exclude them in -// `packages/nextjs/jest.config.js`. The resets the test-matching regex so that when `runTest.ts` calls `yarn jest` with -// ths config file, jest will find the tests in `tests`. -module.exports = require('../../../../jest/jest.config.js'); diff --git a/packages/nextjs/test/buildProcess/runTest.ts b/packages/nextjs/test/buildProcess/runTest.ts deleted file mode 100644 index e541e7d641b1..000000000000 --- a/packages/nextjs/test/buildProcess/runTest.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable no-console */ -import * as childProcess from 'child_process'; -import * as path from 'path'; -import { sync as rimrafSync } from 'rimraf'; - -const TEST_APP_DIR = 'test/buildProcess/testApp'; - -/** - * Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current - * process if this script is run with the `--debug` flag. Returns contents of `stdout`. - */ -function run(cmd: string, options?: childProcess.ExecSyncOptions): string { - return String( - childProcess.execSync(cmd, { - stdio: process.argv.includes('--debug') ? 'inherit' : 'ignore', - ...options, - }), - ); -} - -// Note: We use a file dependency for the SDK, rather than linking it, because if it's linked, nextjs rolls the entire -// SDK and all of its dependencies into a bundle, making it impossible to tell (from the NFT file, at least) what's -// being included. -console.log('Installing dependencies...'); -process.chdir(TEST_APP_DIR); -rimrafSync('node_modules'); -run('yarn'); - -console.log('Building app...'); -rimrafSync('.next'); -run('yarn build'); - -console.log('App built. Running tests...'); -process.chdir('..'); -const jestConfigFile = path.resolve(process.cwd(), 'jest.config.js'); -try { - // We have to specify the config file explicitly because otherwise it'll use the one at the root level of - // `packages/nextjs`, since that's where the closest `package.json` file is - run(`yarn jest --config ${jestConfigFile} tests`, { stdio: 'inherit' }); -} catch (err) { - console.log('\nNot all build process tests passed.'); -} diff --git a/packages/nextjs/test/buildProcess/testApp/.gitignore b/packages/nextjs/test/buildProcess/testApp/.gitignore deleted file mode 100644 index c87c9b392c02..000000000000 --- a/packages/nextjs/test/buildProcess/testApp/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/packages/nextjs/test/buildProcess/testApp/next.config.js b/packages/nextjs/test/buildProcess/testApp/next.config.js deleted file mode 100644 index 0bbf690a9c3b..000000000000 --- a/packages/nextjs/test/buildProcess/testApp/next.config.js +++ /dev/null @@ -1,25 +0,0 @@ -// const nextConfig = { -// } -// -// module.exports = nextConfig - -const { withSentryConfig } = require('@sentry/nextjs'); - -const moduleExports = { - reactStrictMode: true, - swcMinify: true, - eslint: { - ignoreDuringBuilds: true, - }, - sentry: { - // Suppress the warning message from `handleSourcemapHidingOptionWarning` in `src/config/webpack.ts` - // TODO (v8): This can come out in v8, because this option will get a default value - hideSourceMaps: false, - }, -}; -const SentryWebpackPluginOptions = { - dryRun: true, - silent: true, -}; - -module.exports = withSentryConfig(moduleExports, SentryWebpackPluginOptions); diff --git a/packages/nextjs/test/buildProcess/testApp/package.json b/packages/nextjs/test/buildProcess/testApp/package.json deleted file mode 100644 index 1a7747a1335c..000000000000 --- a/packages/nextjs/test/buildProcess/testApp/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "testapp", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@sentry/nextjs": "file:../../../", - "@vercel/nft": "latest", - "next": "13.0.1", - "react": "18.2.0", - "react-dom": "18.2.0" - } -} diff --git a/packages/nextjs/test/buildProcess/testApp/sentry.client.config.js b/packages/nextjs/test/buildProcess/testApp/sentry.client.config.js deleted file mode 100644 index 750af2854977..000000000000 --- a/packages/nextjs/test/buildProcess/testApp/sentry.client.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import * as Sentry from '@sentry/nextjs'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', -}); diff --git a/packages/nextjs/test/buildProcess/testApp/sentry.server.config.js b/packages/nextjs/test/buildProcess/testApp/sentry.server.config.js deleted file mode 100644 index 750af2854977..000000000000 --- a/packages/nextjs/test/buildProcess/testApp/sentry.server.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import * as Sentry from '@sentry/nextjs'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', -}); diff --git a/packages/nextjs/test/buildProcess/testApp/src/dogs.json b/packages/nextjs/test/buildProcess/testApp/src/dogs.json deleted file mode 100644 index cbc2044ae399..000000000000 --- a/packages/nextjs/test/buildProcess/testApp/src/dogs.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "dogs": "are great" -} diff --git a/packages/nextjs/test/buildProcess/testApp/src/pages/_app.js b/packages/nextjs/test/buildProcess/testApp/src/pages/_app.js deleted file mode 100644 index a839a8750b1a..000000000000 --- a/packages/nextjs/test/buildProcess/testApp/src/pages/_app.js +++ /dev/null @@ -1,5 +0,0 @@ -function MyApp({ Component, pageProps }) { - return ; -} - -export default MyApp; diff --git a/packages/nextjs/test/buildProcess/testApp/src/pages/api/dogs.js b/packages/nextjs/test/buildProcess/testApp/src/pages/api/dogs.js deleted file mode 100644 index 8cc795b8c8e4..000000000000 --- a/packages/nextjs/test/buildProcess/testApp/src/pages/api/dogs.js +++ /dev/null @@ -1,8 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -export default function handler(req, res) { - const dogData = JSON.parse(fs.readFileSync(path.resolve('../../dogs.json'))); - dogData.test = 'something'; - res.status(200).json(dogData); -} diff --git a/packages/nextjs/test/buildProcess/testApp/src/pages/index.js b/packages/nextjs/test/buildProcess/testApp/src/pages/index.js deleted file mode 100644 index 2fb9aaeef234..000000000000 --- a/packages/nextjs/test/buildProcess/testApp/src/pages/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function Home() { - return
This is the homepage.
; -} diff --git a/packages/nextjs/test/buildProcess/testApp/yarn.lock b/packages/nextjs/test/buildProcess/testApp/yarn.lock deleted file mode 100644 index 79446821003e..000000000000 --- a/packages/nextjs/test/buildProcess/testApp/yarn.lock +++ /dev/null @@ -1,1165 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@jridgewell/sourcemap-codec@^1.4.13": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== - -"@mapbox/node-pre-gyp@^1.0.5": - version "1.0.10" - resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" - integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA== - dependencies: - detect-libc "^2.0.0" - https-proxy-agent "^5.0.0" - make-dir "^3.1.0" - node-fetch "^2.6.7" - nopt "^5.0.0" - npmlog "^5.0.1" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.11" - -"@next/env@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.0.1.tgz#0361e203c7bfbc7b69679ec48f7b45a8f4cb1c2c" - integrity sha512-gK60YoFae3s8qi5UgIzbvxOhsh5gKyEaiKH5+kLBUYXLlrPyWJR2xKBj2WqvHkO7wDX7/Hed3DAqjSpU4ijIvQ== - -"@next/swc-android-arm-eabi@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.1.tgz#7ce2a7b6576845bc6d7f55504bf9b82a0d9a2792" - integrity sha512-M28QSbohZlNXNn//HY6lV2T3YaMzG58Jwr0YwOdVmOQv6i+7lu6xe3GqQu4kdqInqhLrBXnL+nabFuGTVSHtTg== - -"@next/swc-android-arm64@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.0.1.tgz#85a13d7667042394939741be218076e4e83a45a2" - integrity sha512-szmO/i6GoHcPXcbhUKhwBMETWHNXH3ITz9wfxwOOFBNKdDU8pjKsHL88lg28aOiQYZSU1sxu1v1p9KY5kJIZCg== - -"@next/swc-darwin-arm64@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.1.tgz#d615d286127bb096a8950a9d7180fcc5d307614d" - integrity sha512-O1RxCaiDNOjGZmdAp6SQoHUITt9aVDQXoR3lZ/TloI/NKRAyAV4u0KUUofK+KaZeHOmVTnPUaQuCyZSc3i1x5Q== - -"@next/swc-darwin-x64@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.1.tgz#f410beb8cbe0e82562226309f8ec8924cc6cb410" - integrity sha512-8E6BY/VO+QqQkthhoWgB8mJMw1NcN9Vhl2OwEwxv8jy2r3zjeU+WNRxz4y8RLbcY0R1h+vHlXuP0mLnuac84tQ== - -"@next/swc-freebsd-x64@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.1.tgz#16eb9652d3f638305ca16b558408f5bc5eb6edde" - integrity sha512-ocwoOxm2KVwF50RyoAT+2RQPLlkyoF7sAqzMUVgj+S6+DTkY3iwH+Zpo0XAk2pnqT9qguOrKnEpq9EIx//+K7Q== - -"@next/swc-linux-arm-gnueabihf@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.1.tgz#0da0700ccf654f813b4c86d057a998598a2fd427" - integrity sha512-yO7e3zITfGol/N6lPQnmIRi0WyuILBMXrvH6EdmWzzqMDJFfTCII6l+B6gMO5WVDCTQUGQlQRNZ7sFqWR4I71g== - -"@next/swc-linux-arm64-gnu@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.1.tgz#f34759cd41086f5b8b582081b2af54f67dc544ae" - integrity sha512-OEs6WDPDI8RyM8SjOqTDMqMBfOlU97VnW6ZMXUvzUTyH0K9c7NF+cn7UMu+I4tKFN0uJ9WQs/6TYaFBGkgoVVA== - -"@next/swc-linux-arm64-musl@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.1.tgz#bcfbf1cdfb9f4d632e7ebd67fd62b768cdd08cb7" - integrity sha512-y5ypFK0Y3urZSFoQxbtDqvKsBx026sz+Fm+xHlPWlGHNZrbs3Q812iONjcZTo09QwRMk5X86iMWBRxV18xMhaw== - -"@next/swc-linux-x64-gnu@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.1.tgz#ed77528d4a3195d5e57d5d94d12cb2206c2b19ac" - integrity sha512-XDIHEE6SU8VCF+dUVntD6PDv6RK31N0forx9kucZBYirbe8vCZ+Yx8hYgvtIaGrTcWtGxibxmND0pIuHDq8H5g== - -"@next/swc-linux-x64-musl@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.1.tgz#74cda49229d2a7fa421fee6b7dcd621a57934a5e" - integrity sha512-yxIOuuz5EOx0F1FDtsyzaLgnDym0Ysxv8CWeJyDTKKmt9BVyITg6q/cD+RP9bEkT1TQi+PYXIMATSz675Q82xw== - -"@next/swc-win32-arm64-msvc@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.1.tgz#15d6add92aa897148d6c45749bf9d2eacee87197" - integrity sha512-+ucLe2qgQzP+FM94jD4ns6LDGyMFaX9k3lVHqu/tsQCy2giMymbport4y4p77mYcXEMlDaHMzlHgOQyHRniWFA== - -"@next/swc-win32-ia32-msvc@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.1.tgz#e0c57902fe75327d092abb1ef19657775fe26f85" - integrity sha512-Krr/qGN7OB35oZuvMAZKoXDt2IapynIWLh5A5rz6AODb7f/ZJqyAuZSK12vOa2zKdobS36Qm4IlxxBqn9c00MA== - -"@next/swc-win32-x64-msvc@13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.1.tgz#469dde61519f6a310874af93ee5969f1d5ff6d03" - integrity sha512-t/0G33t/6VGWZUGCOT7rG42qqvf/x+MrFp1CU+8CN6PrjSSL57R5bqkXfubV9t4eCEnUxVP+5Hn3MoEXEebtEw== - -"@rollup/plugin-commonjs@24.0.0": - version "24.0.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz#fb7cf4a6029f07ec42b25daa535c75b05a43f75c" - integrity sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g== - dependencies: - "@rollup/pluginutils" "^5.0.1" - commondir "^1.0.1" - estree-walker "^2.0.2" - glob "^8.0.3" - is-reference "1.2.1" - magic-string "^0.27.0" - -"@rollup/pluginutils@^5.0.1": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33" - integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^2.0.2" - picomatch "^2.3.1" - -"@sentry-internal/tracing@7.80.0": - version "7.80.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.80.0.tgz#f9a6c0456b3cbf4a53c986a0b9208572d80e0756" - integrity sha512-P1Ab9gamHLsbH9D82i1HY8xfq9dP8runvc4g50AAd6OXRKaJ45f2KGRZUmnMEVqBQ7YoPYp2LFMkrhNYbcZEoQ== - dependencies: - "@sentry/core" "7.80.0" - "@sentry/types" "7.80.0" - "@sentry/utils" "7.80.0" - -"@sentry/browser@7.80.0": - version "7.80.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.80.0.tgz#385fb59ac1d52b67919087f3d7044575ae0abbdd" - integrity sha512-Ngwjc+yyf/aH5q7iQM1LeDNlhM1Ilt4ZLUogTghZR/guwNWmCtk3OHcjOLz7fxBBj9wGFUc2pHPyeYM6bQhrEw== - dependencies: - "@sentry-internal/tracing" "7.80.0" - "@sentry/core" "7.80.0" - "@sentry/replay" "7.80.0" - "@sentry/types" "7.80.0" - "@sentry/utils" "7.80.0" - -"@sentry/cli@^1.74.6": - version "1.74.6" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.74.6.tgz#c4f276e52c6f5e8c8d692845a965988068ebc6f5" - integrity sha512-pJ7JJgozyjKZSTjOGi86chIngZMLUlYt2HOog+OJn+WGvqEkVymu8m462j1DiXAnex9NspB4zLLNuZ/R6rTQHg== - dependencies: - https-proxy-agent "^5.0.0" - mkdirp "^0.5.5" - node-fetch "^2.6.7" - npmlog "^4.1.2" - progress "^2.0.3" - proxy-from-env "^1.1.0" - which "^2.0.2" - -"@sentry/core@7.80.0": - version "7.80.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.80.0.tgz#7b8a460c19160b81ade20080333189f1a80c1410" - integrity sha512-nJiiymdTSEyI035/rdD3VOq6FlOZ2wWLR5bit9LK8a3rzHU3UXkwScvEo6zYgs0Xp1sC0yu1S9+0BEiYkmi29A== - dependencies: - "@sentry/types" "7.80.0" - "@sentry/utils" "7.80.0" - -"@sentry/integrations@7.80.0": - version "7.80.0" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.80.0.tgz#d81dc3b357d4efd4368471b39496494ab221a64a" - integrity sha512-9xI+jtqSBrAG/Y2f4OyeJhl6WZR3i0qCXRwqCZoCFCDgN4ZQORc4VBwaC3nW2s9jgfb13FC2FQToGOVrRnsetg== - dependencies: - "@sentry/core" "7.80.0" - "@sentry/types" "7.80.0" - "@sentry/utils" "7.80.0" - localforage "^1.8.1" - -"@sentry/nextjs@file:../../..": - version "7.80.0" - dependencies: - "@rollup/plugin-commonjs" "24.0.0" - "@sentry/core" "7.80.0" - "@sentry/integrations" "7.80.0" - "@sentry/node" "7.80.0" - "@sentry/react" "7.80.0" - "@sentry/types" "7.80.0" - "@sentry/utils" "7.80.0" - "@sentry/vercel-edge" "7.80.0" - "@sentry/webpack-plugin" "1.20.0" - chalk "3.0.0" - resolve "1.22.8" - rollup "2.78.0" - stacktrace-parser "^0.1.10" - -"@sentry/node@7.80.0": - version "7.80.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.80.0.tgz#7e060bc934a58a442b786246d46a5a0dd822ae44" - integrity sha512-J35fqe8J5ac/17ZXT0ML3opYGTOclqYNE9Sybs1y9n6BqacHyzH8By72YrdI03F7JJDHwrcGw+/H8hGpkCwi0Q== - dependencies: - "@sentry-internal/tracing" "7.80.0" - "@sentry/core" "7.80.0" - "@sentry/types" "7.80.0" - "@sentry/utils" "7.80.0" - https-proxy-agent "^5.0.0" - -"@sentry/react@7.80.0": - version "7.80.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.80.0.tgz#ee589ff202174ced45e77dc2714237031ca9c726" - integrity sha512-xoX7fqgY0NZR9Fud/IJ4a3b8Z/HsdwU5SLILi46lV+CWaXS6eFM1E81jG2Vd2EeYIpkH+bMA//XHMEod8LAJcQ== - dependencies: - "@sentry/browser" "7.80.0" - "@sentry/types" "7.80.0" - "@sentry/utils" "7.80.0" - hoist-non-react-statics "^3.3.2" - -"@sentry/replay@7.80.0": - version "7.80.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.80.0.tgz#0626d85af1d8573038d52ae9e244e3e95fa47385" - integrity sha512-wWnpuJq3OaDLp1LutE4oxWXnau04fvwuzBjuaFvOXOV+pB/kn+pDPuVOC5+FH/RMRZ5ftwX5+dF6fojfcLVGCg== - dependencies: - "@sentry-internal/tracing" "7.80.0" - "@sentry/core" "7.80.0" - "@sentry/types" "7.80.0" - "@sentry/utils" "7.80.0" - -"@sentry/types@7.80.0": - version "7.80.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.80.0.tgz#f6896de2d231a7f8d814cf1c981c474240e96d8a" - integrity sha512-4bpMO+2jWiWLDa8zbTASWWNLWe6yhjfPsa7/6VH5y9x1NGtL8oRbqUsTgsvjF3nmeHEMkHQsC8NHPaQ/ibFmZQ== - -"@sentry/utils@7.80.0": - version "7.80.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.80.0.tgz#5bd682fa9a382eea952d4fa3628f0f33e4240ff3" - integrity sha512-XbBCEl6uLvE50ftKwrEo6XWdDaZXHXu+kkHXTPWQEcnbvfZKLuG9V0Hxtxxq3xQgyWmuF05OH1GcqYqiO+v5Yg== - dependencies: - "@sentry/types" "7.80.0" - -"@sentry/vercel-edge@7.80.0": - version "7.80.0" - resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-7.80.0.tgz#674f77e820db066f408d4aab7887f964361706bb" - integrity sha512-Jh7Kg1+zrSbOqPcLmMQGaGWE2ieJcaCVrvuRgVxUCinZlHB2r5RUlXKLqR6GXV+LVqv8NQDIv1wrKfLSHdSKJA== - dependencies: - "@sentry/core" "7.80.0" - "@sentry/types" "7.80.0" - "@sentry/utils" "7.80.0" - -"@sentry/webpack-plugin@1.20.0": - version "1.20.0" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz#e7add76122708fb6b4ee7951294b521019720e58" - integrity sha512-Ssj1mJVFsfU6vMCOM2d+h+KQR7QHSfeIP16t4l20Uq/neqWXZUQ2yvQfe4S3BjdbJXz/X4Rw8Hfy1Sd0ocunYw== - dependencies: - "@sentry/cli" "^1.74.6" - webpack-sources "^2.0.0 || ^3.0.0" - -"@swc/helpers@0.4.11": - version "0.4.11" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.11.tgz#db23a376761b3d31c26502122f349a21b592c8de" - integrity sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw== - dependencies: - tslib "^2.4.0" - -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== - -"@vercel/nft@latest": - version "0.22.1" - resolved "https://registry.yarnpkg.com/@vercel/nft/-/nft-0.22.1.tgz#0d91d2a21e3a7f0b23ce1550da9870eac4942828" - integrity sha512-lYYZIoxRurqDOSoVIdBicGnpUIpfyaS5qVjdPq+EfI285WqtZK3NK/dyCkiyBul+X2U2OEhRyeMdXPCHGJbohw== - dependencies: - "@mapbox/node-pre-gyp" "^1.0.5" - acorn "^8.6.0" - async-sema "^3.1.1" - bindings "^1.4.0" - estree-walker "2.0.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - micromatch "^4.0.2" - node-gyp-build "^4.2.2" - resolve-from "^5.0.0" - rollup-pluginutils "^2.8.2" - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -acorn@^8.6.0: - version "8.8.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" - integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -are-we-there-yet@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" - integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - -are-we-there-yet@~1.1.2: - version "1.1.7" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" - integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -async-sema@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/async-sema/-/async-sema-3.1.1.tgz#e527c08758a0f8f6f9f15f799a173ff3c40ea808" - integrity sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -bindings@^1.4.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -caniuse-lite@^1.0.30001406: - version "1.0.30001431" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795" - integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ== - -chalk@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -client-only@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" - integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-support@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -debug@4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== - -detect-libc@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" - integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -estree-walker@2.0.2, estree-walker@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== - -estree-walker@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" - integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -gauge@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" - integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.2" - console-control-strings "^1.0.0" - has-unicode "^2.0.1" - object-assign "^4.1.1" - signal-exit "^3.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.2" - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg== - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^8.0.3: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -graceful-fs@^4.2.9: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-unicode@^2.0.0, has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - -hasown@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" - integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== - dependencies: - function-bind "^1.1.2" - -hoist-non-react-statics@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-core-module@^2.13.0: - version "2.13.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" - integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== - dependencies: - hasown "^2.0.0" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw== - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-reference@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" - integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== - dependencies: - "@types/estree" "*" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -"js-tokens@^3.0.0 || ^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -lie@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" - integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== - dependencies: - immediate "~3.0.5" - -localforage@^1.8.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" - integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== - dependencies: - lie "3.1.1" - -loose-envify@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -magic-string@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" - integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - -make-dir@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -micromatch@^4.0.2: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimist@^1.2.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== - -minipass@^3.0.0: - version "3.3.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae" - integrity sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw== - dependencies: - yallist "^4.0.0" - -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mkdirp@^0.5.5: - version "0.5.6" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - -mkdirp@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -nanoid@^3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" - integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== - -next@13.0.1: - version "13.0.1" - resolved "https://registry.yarnpkg.com/next/-/next-13.0.1.tgz#8b4fc9998e58f503bdecb92f06fe6f850ac260d0" - integrity sha512-ErCNBPIeZMKFn6hX+ZBSlqZVgJIeitEqhGTuQUNmYXJ07/A71DZ7AJI8eyHYUdBb686LUpV1/oBdTq9RpzRVPg== - dependencies: - "@next/env" "13.0.1" - "@swc/helpers" "0.4.11" - caniuse-lite "^1.0.30001406" - postcss "8.4.14" - styled-jsx "5.1.0" - use-sync-external-store "1.2.0" - optionalDependencies: - "@next/swc-android-arm-eabi" "13.0.1" - "@next/swc-android-arm64" "13.0.1" - "@next/swc-darwin-arm64" "13.0.1" - "@next/swc-darwin-x64" "13.0.1" - "@next/swc-freebsd-x64" "13.0.1" - "@next/swc-linux-arm-gnueabihf" "13.0.1" - "@next/swc-linux-arm64-gnu" "13.0.1" - "@next/swc-linux-arm64-musl" "13.0.1" - "@next/swc-linux-x64-gnu" "13.0.1" - "@next/swc-linux-x64-musl" "13.0.1" - "@next/swc-win32-arm64-msvc" "13.0.1" - "@next/swc-win32-ia32-msvc" "13.0.1" - "@next/swc-win32-x64-msvc" "13.0.1" - -node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-gyp-build@^4.2.2: - version "4.5.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" - integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== - -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - -npmlog@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -npmlog@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" - integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== - dependencies: - are-we-there-yet "^2.0.0" - console-control-strings "^1.1.0" - gauge "^3.0.0" - set-blocking "^2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== - -object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -postcss@8.4.14: - version "8.4.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" - integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== - dependencies: - nanoid "^3.3.4" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -progress@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -react-dom@18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" - integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.0" - -react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react@18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== - dependencies: - loose-envify "^1.1.0" - -readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve@1.22.8: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rollup-pluginutils@^2.8.2: - version "2.8.2" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" - integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== - dependencies: - estree-walker "^0.6.1" - -rollup@2.78.0: - version "2.78.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.0.tgz#00995deae70c0f712ea79ad904d5f6b033209d9e" - integrity sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg== - optionalDependencies: - fsevents "~2.3.2" - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -scheduler@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" - integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== - dependencies: - loose-envify "^1.1.0" - -semver@^6.0.0: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.3.5: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - -signal-exit@^3.0.0: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -stacktrace-parser@^0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" - integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg== - dependencies: - type-fest "^0.7.1" - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw== - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -styled-jsx@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.0.tgz#4a5622ab9714bd3fcfaeec292aa555871f057563" - integrity sha512-/iHaRJt9U7T+5tp6TRelLnqBqiaIT0HsO0+vgyj8hK2KUk7aejFqRrumqPUlAqDwAj8IbS/1hk3IhBAAK/FCUQ== - dependencies: - client-only "0.0.1" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -tar@^6.1.11: - version "6.1.12" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.12.tgz#3b742fb05669b55671fb769ab67a7791ea1a62e6" - integrity sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -tslib@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" - integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== - -type-fest@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" - integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== - -use-sync-external-store@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" - integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -"webpack-sources@^2.0.0 || ^3.0.0": - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0, wide-align@^1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== diff --git a/packages/nextjs/test/buildProcess/tests/nft.test.ts b/packages/nextjs/test/buildProcess/tests/nft.test.ts deleted file mode 100644 index 84226f79d446..000000000000 --- a/packages/nextjs/test/buildProcess/tests/nft.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -import { deepReadDirSync } from '@sentry/node'; - -type NFTFile = { - files: string[]; -}; - -it('excludes build-time SDK dependencies from nft files', () => { - const dogNFTjson = require('../testApp/.next/server/pages/api/dogs.js.nft.json') as NFTFile; - - // These are all of the files which control the way we modify the user's app build by changing the webpack config. - // They get mistakenly included by the nft plugin because we export `withSentryConfig` from `index.server.ts`. Because - // the wrappers are referenced from the code we inject at build time, they need to stay. `withSentryConfig.ts` itself - // also needs to stay because nextjs loads `next.config.js` at runtime - const sentryConfigDirEntries = dogNFTjson.files.filter(entry => - entry.includes('node_modules/@sentry/nextjs/build/cjs/config'), - ); - const sentryWrapperEntries = sentryConfigDirEntries.filter(entry => entry.includes('config/wrappers')); - const excludedSentryConfigEntries = sentryConfigDirEntries.filter( - entry => !sentryWrapperEntries.includes(entry) && !entry.includes('withSentryConfig'), - ); - - // Sucrase and rollup are dependencies of one of the webpack loaders we add to the config - also not needed at runtime. - const sucraseEntries = dogNFTjson.files.filter(entry => entry.includes('node_modules/sucrase')); - const rollupEntries = dogNFTjson.files.filter( - entry => entry.includes('node_modules/rollup') || entry.includes('node_modules/@rollup'), - ); - - // None of the build-time dependencies should be listed - expect(excludedSentryConfigEntries.length).toEqual(0); - expect(sucraseEntries.length).toEqual(0); - expect(rollupEntries.length).toEqual(0); - - // We don't want to accidentally remove the wrappers - // eslint-disable-next-line deprecation/deprecation - const wrapperFiles = deepReadDirSync('src/config/wrappers/').filter(filename => filename !== 'types.ts'); - expect(sentryWrapperEntries.length).toEqual(wrapperFiles.length); -}); diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index fd43f1bee9ad..bc6732db9c5a 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -1,4 +1,4 @@ -import { BaseClient, getCurrentHub } from '@sentry/core'; +import { BaseClient, getClient } from '@sentry/core'; import * as SentryReact from '@sentry/react'; import { BrowserTracing, WINDOW, getCurrentScope } from '@sentry/react'; import type { Integration } from '@sentry/types'; @@ -70,7 +70,7 @@ describe('Client init()', () => { }); it('sets runtime on scope', () => { - const currentScope = getCurrentHub().getScope(); + const currentScope = getCurrentScope(); // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); @@ -86,10 +86,10 @@ describe('Client init()', () => { dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', tracesSampleRate: 1.0, }); - const hub = getCurrentHub(); - const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); + const transportSend = jest.spyOn(getClient()!.getTransport()!, 'send'); // Ensure we have no current span, so our next span is a transaction + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(undefined); SentryReact.startSpan({ name: '/404' }, () => { diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts index da43ec724944..91b61516a240 100644 --- a/packages/nextjs/test/config/withSentry.test.ts +++ b/packages/nextjs/test/config/withSentry.test.ts @@ -1,5 +1,5 @@ import * as SentryCore from '@sentry/core'; -import { addTracingExtensions } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions } from '@sentry/core'; import type { NextApiRequest, NextApiResponse } from 'next'; import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/common/types'; @@ -45,8 +45,10 @@ describe('withSentry', () => { name: 'GET http://dogs.are.great', op: 'http.server', origin: 'auto.http.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, metadata: { - source: 'route', request: expect.objectContaining({ url: 'http://dogs.are.great' }), }, }, diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts index 2a782250c7c5..97d6e7b103e1 100644 --- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts +++ b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts @@ -1,4 +1,5 @@ import * as coreSdk from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { withEdgeWrapping } from '../../src/common/utils/edgeWrapperUtils'; @@ -81,7 +82,12 @@ describe('withEdgeWrapping', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ - metadata: { request: { headers: {} }, source: 'route' }, + metadata: { + request: { headers: {} }, + }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, name: 'some label', op: 'some op', origin: 'auto.function.nextjs.withEdgeWrapping', diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts index b51ce3dfeca6..ea5e7c4319f0 100644 --- a/packages/nextjs/test/edge/withSentryAPI.test.ts +++ b/packages/nextjs/test/edge/withSentryAPI.test.ts @@ -1,4 +1,5 @@ import * as coreSdk from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { wrapApiHandlerWithSentry } from '../../src/edge'; @@ -52,7 +53,12 @@ describe('wrapApiHandlerWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ - metadata: { request: { headers: {}, method: 'POST', url: 'https://sentry.io/' }, source: 'route' }, + metadata: { + request: { headers: {}, method: 'POST', url: 'https://sentry.io/' }, + }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, name: 'POST /user/[userId]/post/[postId]', op: 'http.server', origin: 'auto.function.nextjs.withEdgeWrapping', @@ -71,7 +77,10 @@ describe('wrapApiHandlerWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ - metadata: { source: 'route' }, + metadata: {}, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, name: 'handler (/user/[userId]/post/[postId])', op: 'http.server', origin: 'auto.function.nextjs.withEdgeWrapping', diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 6201ddf34a53..b31699b12fb3 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -1,6 +1,6 @@ import { runWithAsyncContext } from '@sentry/core'; import * as SentryNode from '@sentry/node'; -import { NodeClient, getCurrentHub } from '@sentry/node'; +import { NodeClient, getClient, getCurrentHub, getCurrentScope } from '@sentry/node'; import type { Integration } from '@sentry/types'; import { GLOBAL_OBJ, logger } from '@sentry/utils'; @@ -71,7 +71,7 @@ describe('Server init()', () => { }); it('sets runtime on scope', () => { - const currentScope = getCurrentHub().getScope(); + const currentScope = getCurrentScope(); // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); @@ -87,7 +87,7 @@ describe('Server init()', () => { // is resolved when importing. it('does not apply `vercel` tag when not running on vercel', () => { - const currentScope = getCurrentHub().getScope(); + const currentScope = getCurrentScope(); expect(process.env.VERCEL).toBeUndefined(); @@ -102,8 +102,7 @@ describe('Server init()', () => { dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', tracesSampleRate: 1.0, }); - const hub = getCurrentHub(); - const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); + const transportSend = jest.spyOn(getClient()!.getTransport()!, 'send'); SentryNode.startSpan({ name: '/404' }, () => { // noop @@ -124,7 +123,9 @@ describe('Server init()', () => { // If we call runWithAsyncContext before init, it executes the callback in the same context as there is no // strategy yet expect(globalHub2).toBe(globalHub); + // eslint-disable-next-line deprecation/deprecation expect(globalHub.getClient()).toBeUndefined(); + // eslint-disable-next-line deprecation/deprecation expect(globalHub2.getClient()).toBeUndefined(); init({}); @@ -132,13 +133,18 @@ describe('Server init()', () => { runWithAsyncContext(() => { const domainHub = getCurrentHub(); // this tag should end up only in the domain hub + // eslint-disable-next-line deprecation/deprecation domainHub.setTag('dogs', 'areGreat'); + // eslint-disable-next-line deprecation/deprecation expect(globalHub.getClient()).toEqual(expect.any(NodeClient)); + // eslint-disable-next-line deprecation/deprecation expect(domainHub.getClient()).toBe(globalHub.getClient()); // @ts-expect-error need access to protected _tags attribute + // eslint-disable-next-line deprecation/deprecation expect(globalHub.getScope()._tags).toEqual({ runtime: 'node' }); // @ts-expect-error need access to protected _tags attribute + // eslint-disable-next-line deprecation/deprecation expect(domainHub.getScope()._tags).toEqual({ runtime: 'node', dogs: 'areGreat' }); }); }); diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index 91b5be0e4145..eddc3e2458af 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node-experimental", - "version": "7.92.0", + "version": "7.93.0", "description": "Experimental version of a Node SDK using OpenTelemetry for performance instrumentation", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-experimental", @@ -48,11 +48,11 @@ "@opentelemetry/sdk-trace-base": "~1.17.1", "@opentelemetry/semantic-conventions": "~1.17.1", "@prisma/instrumentation": "5.4.2", - "@sentry/core": "7.92.0", - "@sentry/node": "7.92.0", - "@sentry/opentelemetry": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/core": "7.93.0", + "@sentry/node": "7.93.0", + "@sentry/opentelemetry": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "optionalDependencies": { "opentelemetry-instrumentation-fetch-node": "1.1.0" diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index f9d065de52db..d44fb10da9c1 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -52,7 +52,9 @@ export { extractRequestData, // eslint-disable-next-line deprecation/deprecation deepReadDirSync, + // eslint-disable-next-line deprecation/deprecation getModuleFromFilename, + createGetModuleFromFilename, close, createTransport, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/node-experimental/src/sdk/api.ts b/packages/node-experimental/src/sdk/api.ts index 71a08dd38552..abb1e37944ac 100644 --- a/packages/node-experimental/src/sdk/api.ts +++ b/packages/node-experimental/src/sdk/api.ts @@ -1,7 +1,6 @@ // PUBLIC APIS import { context } from '@opentelemetry/api'; -import { DEFAULT_ENVIRONMENT, closeSession, makeSession, updateSession } from '@sentry/core'; import type { Breadcrumb, BreadcrumbHint, @@ -12,13 +11,12 @@ import type { Extra, Extras, Primitive, - Session, Severity, SeverityLevel, User, } from '@sentry/types'; -import { GLOBAL_OBJ, consoleSandbox, dateTimestampInSeconds } from '@sentry/utils'; -import { getScopesFromContext, setScopesOnContext } from '../utils/contextData'; +import { consoleSandbox, dateTimestampInSeconds } from '@sentry/utils'; +import { getContextFromScope, getScopesFromContext, setScopesOnContext } from '../utils/contextData'; import type { ExclusiveEventHintOrCaptureContext } from '../utils/prepareEvent'; import { parseEventHintOrCaptureContext } from '../utils/prepareEvent'; @@ -29,9 +27,39 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, getClient }; export { setCurrentScope, setIsolationScope } from './scope'; /** - * Fork a scope from the current scope, and make it the current scope in the given callback + * Creates a new scope with and executes the given operation within. + * The scope is automatically removed once the operation + * finishes or throws. + * + * This is essentially a convenience function for: + * + * pushScope(); + * callback(); + * popScope(); */ -export function withScope(callback: (scope: Scope) => T): T { +export function withScope(callback: (scope: Scope) => T): T; +/** + * Set the given scope as the active scope in the callback. + */ +export function withScope(scope: Scope | undefined, callback: (scope: Scope) => T): T; +/** + * Either creates a new active scope, or sets the given scope as active scope in the given callback. + */ +export function withScope( + ...rest: [callback: (scope: Scope) => T] | [scope: Scope | undefined, callback: (scope: Scope) => T] +): T { + // If a scope is defined, we want to make this the active scope instead of the default one + if (rest.length === 2) { + const [scope, callback] = rest; + if (!scope) { + return context.with(context.active(), () => callback(getCurrentScope())); + } + + const ctx = getContextFromScope(scope); + return context.with(ctx || context.active(), () => callback(getCurrentScope())); + } + + const callback = rest[0]; return context.with(context.active(), () => callback(getCurrentScope())); } @@ -61,6 +89,7 @@ export function withIsolationScope(callback: (isolationScope: Scope) => T): T * @deprecated This function will be removed in the next major version of the Sentry SDK. */ export function lastEventId(): string | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().lastEventId(); } @@ -168,60 +197,3 @@ export function setContext( ): void { getIsolationScope().setContext(name, context); } - -/** Start a session on the current isolation scope. */ -export function startSession(context?: Session): Session { - const client = getClient(); - const isolationScope = getIsolationScope(); - - const { release, environment = DEFAULT_ENVIRONMENT } = client.getOptions(); - - // Will fetch userAgent if called from browser sdk - const { userAgent } = GLOBAL_OBJ.navigator || {}; - - const session = makeSession({ - release, - environment, - user: isolationScope.getUser(), - ...(userAgent && { userAgent }), - ...context, - }); - - // End existing session if there's one - const currentSession = isolationScope.getSession && isolationScope.getSession(); - if (currentSession && currentSession.status === 'ok') { - updateSession(currentSession, { status: 'exited' }); - } - endSession(); - - // Afterwards we set the new session on the scope - isolationScope.setSession(session); - - return session; -} - -/** End the session on the current isolation scope. */ -export function endSession(): void { - const isolationScope = getIsolationScope(); - const session = isolationScope.getSession(); - if (session) { - closeSession(session); - } - _sendSessionUpdate(); - - // the session is over; take it off of the scope - isolationScope.setSession(); -} - -/** - * Sends the current Session on the scope - */ -function _sendSessionUpdate(): void { - const scope = getCurrentScope(); - const client = getClient(); - - const session = scope.getSession(); - if (session && client.captureSession) { - client.captureSession(session); - } -} diff --git a/packages/node-experimental/src/sdk/hub.ts b/packages/node-experimental/src/sdk/hub.ts index b58548acb326..b2ffaa16b364 100644 --- a/packages/node-experimental/src/sdk/hub.ts +++ b/packages/node-experimental/src/sdk/hub.ts @@ -10,11 +10,11 @@ import type { TransactionContext, } from '@sentry/types'; +import { endSession, startSession } from '@sentry/core'; import { addBreadcrumb, captureEvent, configureScope, - endSession, getClient, getCurrentScope, lastEventId, @@ -24,7 +24,6 @@ import { setTag, setTags, setUser, - startSession, withScope, } from './api'; import { callExtensionMethod, getGlobalCarrier } from './globals'; @@ -95,6 +94,7 @@ export function getCurrentHub(): Hub { }, getIntegration(integration: IntegrationClass): T | null { + // eslint-disable-next-line deprecation/deprecation return getClient().getIntegration(integration); }, @@ -121,6 +121,7 @@ export function getCurrentHub(): Hub { captureSession(endSession?: boolean): void { // both send the update and pull the session from the scope if (endSession) { + // eslint-disable-next-line deprecation/deprecation return this.endSession(); } diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index 821757a9a246..ce22aa109339 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -1,4 +1,4 @@ -import { getIntegrationsToSetup, hasTracingEnabled } from '@sentry/core'; +import { endSession, getIntegrationsToSetup, hasTracingEnabled, startSession } from '@sentry/core'; import { Integrations, defaultIntegrations as defaultNodeIntegrations, @@ -6,7 +6,7 @@ import { getSentryRelease, makeNodeTransport, } from '@sentry/node'; -import type { Integration } from '@sentry/types'; +import type { Client, Integration } from '@sentry/types'; import { consoleSandbox, dropUndefinedKeys, @@ -22,7 +22,7 @@ import { Http } from '../integrations/http'; import { NodeFetch } from '../integrations/node-fetch'; import { setOpenTelemetryContextAsyncContextStrategy } from '../otel/asyncContextStrategy'; import type { NodeExperimentalClientOptions, NodeExperimentalOptions } from '../types'; -import { endSession, getClient, getCurrentScope, getGlobalScope, getIsolationScope, startSession } from './api'; +import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from './api'; import { NodeExperimentalClient } from './client'; import { getGlobalCarrier } from './globals'; import { setLegacyHubOnCarrier } from './hub'; @@ -67,7 +67,9 @@ export function init(options: NodeExperimentalOptions | undefined = {}): void { // unless somebody specifically sets a different one on a scope/isolations cope getGlobalScope().setClient(client); - client.setupIntegrations(); + if (isEnabled(client)) { + client.init(); + } if (options.autoSessionTracking) { startSessionTracking(); @@ -79,7 +81,11 @@ export function init(options: NodeExperimentalOptions | undefined = {}): void { const client = getClient(); if (client.addIntegration) { // force integrations to be setup even if no DSN was set - client.setupIntegrations(true); + // If they have already been added before, they will be ignored anyhow + const integrations = client.getOptions().integrations; + for (const integration of integrations) { + client.addIntegration(integration); + } client.addIntegration( new Integrations.Spotlight({ sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, @@ -213,3 +219,7 @@ function startSessionTracking(): void { } }); } + +function isEnabled(client: Client): boolean { + return client.getOptions().enabled !== false && client.getTransport() !== undefined; +} diff --git a/packages/node-experimental/src/sdk/spanProcessor.ts b/packages/node-experimental/src/sdk/spanProcessor.ts index 175f3681479b..73f741505444 100644 --- a/packages/node-experimental/src/sdk/spanProcessor.ts +++ b/packages/node-experimental/src/sdk/spanProcessor.ts @@ -4,8 +4,8 @@ import type { Span } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { SentrySpanProcessor, getClient, getSpanFinishScope } from '@sentry/opentelemetry'; -import { Http } from '../integrations/http'; -import { NodeFetch } from '../integrations/node-fetch'; +import type { Http } from '../integrations/http'; +import type { NodeFetch } from '../integrations/node-fetch'; import type { NodeExperimentalClient } from '../types'; import { getIsolationScope } from './api'; import { Scope } from './scope'; @@ -33,8 +33,8 @@ export class NodeExperimentalSentrySpanProcessor extends SentrySpanProcessor { /** @inheritDoc */ protected _shouldSendSpanToSentry(span: Span): boolean { const client = getClient(); - const httpIntegration = client ? client.getIntegration(Http) : undefined; - const fetchIntegration = client ? client.getIntegration(NodeFetch) : undefined; + const httpIntegration = client ? client.getIntegrationByName('Http') : undefined; + const fetchIntegration = client ? client.getIntegrationByName('NodeFetch') : undefined; // If we encounter a client or server span with url & method, we assume this comes from the http instrumentation // In this case, if `shouldCreateSpansForRequests` is false, we want to _record_ the span but not _sample_ it, diff --git a/packages/node-experimental/src/sdk/types.ts b/packages/node-experimental/src/sdk/types.ts index 90c61dceda86..57ad321a5470 100644 --- a/packages/node-experimental/src/sdk/types.ts +++ b/packages/node-experimental/src/sdk/types.ts @@ -2,8 +2,6 @@ import type { Attachment, Breadcrumb, Contexts, - Event, - EventHint, EventProcessor, Extras, Hub, @@ -11,7 +9,6 @@ import type { Primitive, PropagationContext, Scope as BaseScope, - Severity, SeverityLevel, User, } from '@sentry/types'; @@ -35,14 +32,9 @@ export interface Scope extends BaseScope { isolationScope: typeof this | undefined; // @ts-expect-error typeof this is what we want here clone(scope?: Scope): typeof this; - captureException(exception: unknown, hint?: EventHint): string; - captureMessage( - message: string, - // eslint-disable-next-line deprecation/deprecation - level?: Severity | SeverityLevel, - hint?: EventHint, - ): string; - captureEvent(event: Event, hint?: EventHint): string; + /** + * @deprecated This function will be removed in the next major version of the Sentry SDK. + */ lastEventId(): string | undefined; getScopeData(): ScopeData; } diff --git a/packages/node-experimental/src/utils/contextData.ts b/packages/node-experimental/src/utils/contextData.ts index 5c69f186eb6d..cb77d37a9ae0 100644 --- a/packages/node-experimental/src/utils/contextData.ts +++ b/packages/node-experimental/src/utils/contextData.ts @@ -1,10 +1,13 @@ import type { Context } from '@opentelemetry/api'; import { createContextKey } from '@opentelemetry/api'; +import type { Scope } from '@sentry/types'; import type { CurrentScopes } from '../sdk/types'; export const SENTRY_SCOPES_CONTEXT_KEY = createContextKey('sentry_scopes'); +const SCOPE_CONTEXT_MAP = new WeakMap(); + /** * Try to get the current scopes from the given OTEL context. * This requires a Context Manager that was wrapped with getWrappedContextManager. @@ -18,5 +21,17 @@ export function getScopesFromContext(context: Context): CurrentScopes | undefine * This will return a forked context with the Propagation Context set. */ export function setScopesOnContext(context: Context, scopes: CurrentScopes): Context { + // So we can look up the context from the scope later + SCOPE_CONTEXT_MAP.set(scopes.scope, context); + SCOPE_CONTEXT_MAP.set(scopes.isolationScope, context); + return context.setValue(SENTRY_SCOPES_CONTEXT_KEY, scopes); } + +/** + * Get the context related to a scope. + * TODO v8: Use this for the `trace` functions. + * */ +export function getContextFromScope(scope: Scope): Context | undefined { + return SCOPE_CONTEXT_MAP.get(scope); +} diff --git a/packages/node-experimental/test/integration/breadcrumbs.test.ts b/packages/node-experimental/test/integration/breadcrumbs.test.ts index fea78a353011..e80c2e1a90e2 100644 --- a/packages/node-experimental/test/integration/breadcrumbs.test.ts +++ b/packages/node-experimental/test/integration/breadcrumbs.test.ts @@ -1,5 +1,5 @@ import { captureException, withScope } from '@sentry/core'; -import { getCurrentHub, startSpan } from '@sentry/opentelemetry'; +import { startSpan } from '@sentry/opentelemetry'; import { addBreadcrumb, getClient, withIsolationScope } from '../../src/sdk/api'; import type { NodeExperimentalClient } from '../../src/types'; @@ -19,15 +19,15 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); - hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); - hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + addBreadcrumb({ timestamp: 123455, message: 'test3' }); const error = new Error('test'); - hub.captureException(error); + // eslint-disable-next-line deprecation/deprecation + captureException(error); await client.flush(); @@ -102,23 +102,23 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; const error = new Error('test'); startSpan({ name: 'test' }, () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); startSpan({ name: 'inner1' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); }); startSpan({ name: 'inner2' }, () => { - hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + addBreadcrumb({ timestamp: 123455, message: 'test3' }); }); - hub.captureException(error); + // eslint-disable-next-line deprecation/deprecation + captureException(error); }); await client.flush(); @@ -148,30 +148,30 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; const error = new Error('test'); withIsolationScope(() => { startSpan({ name: 'test1' }, () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1-a' }); + addBreadcrumb({ timestamp: 123456, message: 'test1-a' }); startSpan({ name: 'inner1' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test1-b' }); + addBreadcrumb({ timestamp: 123457, message: 'test1-b' }); }); }); }); withIsolationScope(() => { startSpan({ name: 'test2' }, () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test2-a' }); + addBreadcrumb({ timestamp: 123456, message: 'test2-a' }); startSpan({ name: 'inner2' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); }); - hub.captureException(error); + // eslint-disable-next-line deprecation/deprecation + captureException(error); }); }); @@ -201,20 +201,20 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; const error = new Error('test'); startSpan({ name: 'test1' }, () => { withScope(() => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); }); startSpan({ name: 'inner1' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + addBreadcrumb({ timestamp: 123457, message: 'test2' }); }); - hub.captureException(error); + // eslint-disable-next-line deprecation/deprecation + captureException(error); }); await client.flush(); @@ -243,36 +243,36 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; const error = new Error('test'); startSpan({ name: 'test1' }, () => { withScope(() => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); }); startSpan({ name: 'inner1' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + addBreadcrumb({ timestamp: 123457, message: 'test2' }); startSpan({ name: 'inner2' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test3' }); + addBreadcrumb({ timestamp: 123457, message: 'test3' }); startSpan({ name: 'inner3' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test4' }); + addBreadcrumb({ timestamp: 123457, message: 'test4' }); - hub.captureException(error); + // eslint-disable-next-line deprecation/deprecation + captureException(error); startSpan({ name: 'inner4' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test5' }); + addBreadcrumb({ timestamp: 123457, message: 'test5' }); }); - hub.addBreadcrumb({ timestamp: 123457, message: 'test6' }); + addBreadcrumb({ timestamp: 123457, message: 'test6' }); }); }); }); - hub.addBreadcrumb({ timestamp: 123456, message: 'test99' }); + addBreadcrumb({ timestamp: 123456, message: 'test99' }); }); await client.flush(); @@ -302,39 +302,39 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; const error = new Error('test'); const promise1 = withIsolationScope(async () => { await startSpan({ name: 'test' }, async () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); await startSpan({ name: 'inner1' }, async () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + addBreadcrumb({ timestamp: 123457, message: 'test2' }); }); await startSpan({ name: 'inner2' }, async () => { - hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + addBreadcrumb({ timestamp: 123455, message: 'test3' }); }); await new Promise(resolve => setTimeout(resolve, 10)); - hub.captureException(error); + // eslint-disable-next-line deprecation/deprecation + captureException(error); }); }); const promise2 = withIsolationScope(async () => { await startSpan({ name: 'test-b' }, async () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); + addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); await startSpan({ name: 'inner1b' }, async () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); }); await startSpan({ name: 'inner2b' }, async () => { - hub.addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); + addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); }); }); }); diff --git a/packages/node-experimental/test/integration/scope.test.ts b/packages/node-experimental/test/integration/scope.test.ts index 3efb3d09506d..d1170dd636e8 100644 --- a/packages/node-experimental/test/integration/scope.test.ts +++ b/packages/node-experimental/test/integration/scope.test.ts @@ -1,5 +1,5 @@ -import { setGlobalScope } from '@sentry/core'; -import { getCurrentHub, getSpanScope } from '@sentry/opentelemetry'; +import { getCurrentScope, setGlobalScope } from '@sentry/core'; +import { getClient, getSpanScope } from '@sentry/opentelemetry'; import * as Sentry from '../../src/'; import type { NodeExperimentalClient } from '../../src/types'; @@ -20,10 +20,9 @@ describe('Integration | Scope', () => { mockSdkInit({ enableTracing, beforeSend, beforeSendTransaction }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; - const rootScope = hub.getScope(); + const rootScope = getCurrentScope(); const error = new Error('test error'); let spanId: string | undefined; @@ -122,10 +121,8 @@ describe('Integration | Scope', () => { mockSdkInit({ enableTracing, beforeSend, beforeSendTransaction }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; - - const rootScope = hub.getScope(); + const client = getClient() as NodeExperimentalClient; + const rootScope = getCurrentScope(); const error1 = new Error('test error 1'); const error2 = new Error('test error 2'); diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index be48f5f9e6b5..374f104a5c2d 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -1,7 +1,8 @@ import { SpanKind, TraceFlags, context, trace } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { SentrySpanProcessor, getCurrentHub, setPropagationContextOnContext } from '@sentry/opentelemetry'; +import { spanToJSON } from '@sentry/core'; +import { SentrySpanProcessor, getClient, setPropagationContextOnContext } from '@sentry/opentelemetry'; import type { Integration, PropagationContext, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -145,7 +146,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', @@ -337,8 +338,7 @@ describe('Integration | Transactions', () => { mockSdkInit({ enableTracing: true, beforeSendTransaction }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; // We simulate the correct context we'd normally get from the SentryPropagator context.with( @@ -399,7 +399,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', @@ -437,8 +437,7 @@ describe('Integration | Transactions', () => { mockSdkInit({ enableTracing: true, beforeSendTransaction }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; const provider = getProvider(); const multiSpanProcessor = provider?.activeSpanProcessor as | (SpanProcessor & { _spanProcessors?: SpanProcessor[] }) @@ -511,11 +510,10 @@ describe('Integration | Transactions', () => { jest.useFakeTimers(); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; - jest.spyOn(client, 'getIntegration').mockImplementation(integrationClass => { - if (integrationClass.name === 'Http') { + jest.spyOn(client, 'getIntegrationByName').mockImplementation(name => { + if (name === 'Http') { return { shouldCreateSpansForRequests: false, } as Http; @@ -576,11 +574,10 @@ describe('Integration | Transactions', () => { jest.useFakeTimers(); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient() as NodeExperimentalClient; - jest.spyOn(client, 'getIntegration').mockImplementation(integrationClass => { - if (integrationClass.name === 'NodeFetch') { + jest.spyOn(client, 'getIntegrationByName').mockImplementation(name => { + if (name === 'NodeFetch') { return { shouldCreateSpansForRequests: false, } as NodeFetch; diff --git a/packages/node/package.json b/packages/node/package.json index afa3889c0a26..00d70742cd85 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Node.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -29,10 +29,10 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "7.92.0", - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry-internal/tracing": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "https-proxy-agent": "^5.0.0" }, "devDependencies": { diff --git a/packages/node/src/cron/cron.ts b/packages/node/src/cron/cron.ts index a8b42ec0fed7..88a3e9e58eb5 100644 --- a/packages/node/src/cron/cron.ts +++ b/packages/node/src/cron/cron.ts @@ -56,6 +56,8 @@ const ERROR_TEXT = 'Automatic instrumentation of CronJob only supports crontab s * ``` */ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: string): T { + let jobScheduled = false; + return new Proxy(lib, { construct(target, args: ConstructorParameters) { const [cronTime, onTick, onComplete, start, timeZone, ...rest] = args; @@ -64,6 +66,12 @@ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: stri throw new Error(ERROR_TEXT); } + if (jobScheduled) { + throw new Error(`A job named '${monitorSlug}' has already been scheduled`); + } + + jobScheduled = true; + const cronString = replaceCronNames(cronTime); function monitoredTick(context: unknown, onComplete?: unknown): void | Promise { @@ -90,6 +98,12 @@ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: stri throw new Error(ERROR_TEXT); } + if (jobScheduled) { + throw new Error(`A job named '${monitorSlug}' has already been scheduled`); + } + + jobScheduled = true; + const cronString = replaceCronNames(cronTime); param.onTick = (context: unknown, onComplete?: unknown) => { diff --git a/packages/node/src/cron/node-cron.ts b/packages/node/src/cron/node-cron.ts index 2f422b9a85f8..4495a0b54909 100644 --- a/packages/node/src/cron/node-cron.ts +++ b/packages/node/src/cron/node-cron.ts @@ -2,12 +2,12 @@ import { withMonitor } from '@sentry/core'; import { replaceCronNames } from './common'; export interface NodeCronOptions { - name?: string; + name: string; timezone?: string; } export interface NodeCron { - schedule: (cronExpression: string, callback: () => void, options?: NodeCronOptions) => unknown; + schedule: (cronExpression: string, callback: () => void, options: NodeCronOptions) => unknown; } /** diff --git a/packages/node/src/cron/node-schedule.ts b/packages/node/src/cron/node-schedule.ts new file mode 100644 index 000000000000..79ae44a06e52 --- /dev/null +++ b/packages/node/src/cron/node-schedule.ts @@ -0,0 +1,60 @@ +import { withMonitor } from '@sentry/core'; +import { replaceCronNames } from './common'; + +export interface NodeSchedule { + scheduleJob( + nameOrExpression: string | Date | object, + expressionOrCallback: string | Date | object | (() => void), + callback?: () => void, + ): unknown; +} + +/** + * Instruments the `node-schedule` library to send a check-in event to Sentry for each job execution. + * + * ```ts + * import * as Sentry from '@sentry/node'; + * import * as schedule from 'node-schedule'; + * + * const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule); + * + * const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * *', () => { + * console.log('You will see this message every minute'); + * }); + * ``` + */ +export function instrumentNodeSchedule(lib: T & NodeSchedule): T { + return new Proxy(lib, { + get(target, prop: keyof NodeSchedule) { + if (prop === 'scheduleJob') { + // eslint-disable-next-line @typescript-eslint/unbound-method + return new Proxy(target.scheduleJob, { + apply(target, thisArg, argArray: Parameters) { + const [nameOrExpression, expressionOrCallback] = argArray; + + if (typeof nameOrExpression !== 'string' || typeof expressionOrCallback !== 'string') { + throw new Error( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + } + + const monitorSlug = nameOrExpression; + const expression = expressionOrCallback; + + return withMonitor( + monitorSlug, + () => { + return target.apply(thisArg, argArray); + }, + { + schedule: { type: 'crontab', value: replaceCronNames(expression) }, + }, + ); + }, + }); + } + + return target[prop]; + }, + }); +} diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 6d4c6f5a4494..892aabd2dd84 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,9 +1,11 @@ import type * as http from 'http'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, continueTrace, flush, + getActiveSpan, getClient, getCurrentScope, hasTracingEnabled, @@ -71,14 +73,17 @@ export function tracingHandler(): ( op: 'http.server', origin: 'auto.http.node.tracingHandler', ...ctx, + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...ctx.metadata, // The request should already have been stored in `scope.sdkProcessingMetadata` (which will become // `event.sdkProcessingMetadata` the same way the metadata here will) by `sentryRequestMiddleware`, but on the // off chance someone is using `sentryTracingMiddleware` without `sentryRequestMiddleware`, it doesn't hurt to // be sure request: req, - source, }, }, // extra context passed to the tracesSampler @@ -87,6 +92,7 @@ export function tracingHandler(): ( ); // We put the transaction on the scope so users can attach children to it + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); // We also set __sentry_transaction on the response so people can grab the transaction there to add @@ -271,7 +277,8 @@ export function errorHandler(options?: { // For some reason we need to set the transaction on the scope again // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const transaction = (res as any).__sentry_transaction as Span; - if (transaction && _scope.getSpan() === undefined) { + if (transaction && !getActiveSpan()) { + // eslint-disable-next-line deprecation/deprecation _scope.setSpan(transaction); } @@ -328,11 +335,12 @@ interface TrpcMiddlewareArguments { export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { return function ({ path, type, next, rawInput }: TrpcMiddlewareArguments): T { const clientOptions = getClient()?.getOptions(); + // eslint-disable-next-line deprecation/deprecation const sentryTransaction = getCurrentScope().getTransaction(); if (sentryTransaction) { sentryTransaction.updateName(`trpc/${path}`); - sentryTransaction.setMetadata({ source: 'route' }); + sentryTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); sentryTransaction.op = 'rpc.server'; const trpcContext: Record = { @@ -343,6 +351,8 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { trpcContext.input = normalize(rawInput); } + // TODO: Can we rewrite this to an attribute? Or set this on the scope? + // eslint-disable-next-line deprecation/deprecation sentryTransaction.setContext('trpc', trpcContext); } diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index b1811c6fba5d..77289ce68152 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -38,6 +38,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -64,6 +65,7 @@ export { // eslint-disable-next-line deprecation/deprecation trace, withScope, + withIsolationScope, captureCheckIn, withMonitor, setMeasurement, @@ -85,7 +87,14 @@ export { defaultIntegrations, init, defaultStackParser, getSentryRelease } from export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData, parameterize } from '@sentry/utils'; // eslint-disable-next-line deprecation/deprecation export { deepReadDirSync } from './utils'; -export { getModuleFromFilename } from './module'; + +import { createGetModuleFromFilename } from './module'; +/** + * @deprecated use `createGetModuleFromFilename` instead. + */ +export const getModuleFromFilename = createGetModuleFromFilename(); +export { createGetModuleFromFilename }; + // eslint-disable-next-line deprecation/deprecation export { enableAnrDetection } from './integrations/anr/legacy'; @@ -124,9 +133,11 @@ export { hapiErrorPlugin } from './integrations/hapi'; import { instrumentCron } from './cron/cron'; import { instrumentNodeCron } from './cron/node-cron'; +import { instrumentNodeSchedule } from './cron/node-schedule'; /** Methods to instrument cron libraries for Sentry check-ins */ export const cron = { instrumentCron, instrumentNodeCron, + instrumentNodeSchedule, }; diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 3aa71f0589f3..1ebca2fdff57 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -55,9 +55,11 @@ const INTEGRATION_NAME = 'Anr'; const anrIntegration = ((options: Partial = {}) => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client: NodeClient) { - if (NODE_VERSION.major < 16) { - throw new Error('ANR detection requires Node 16 or later'); + if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { + throw new Error('ANR detection requires Node 16.17.0 or later'); } // setImmediate is used to ensure that all other integrations have been setup @@ -68,6 +70,8 @@ const anrIntegration = ((options: Partial = {}) => { /** * Starts a thread to detect App Not Responding (ANR) events + * + * ANR detection requires Node 16.17.0 or later */ // eslint-disable-next-line deprecation/deprecation export const Anr = convertIntegrationFnToClass(INTEGRATION_NAME, anrIntegration); diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index f87ff8bb672f..2e23f823891c 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -15,6 +15,8 @@ import { } from '@sentry/utils'; import { Session as InspectorSession } from 'inspector'; import { parentPort, workerData } from 'worker_threads'; + +import { createGetModuleFromFilename } from '../../module'; import { makeNodeTransport } from '../../transports'; import type { WorkerStartData } from './common'; @@ -46,9 +48,10 @@ async function sendAbnormalSession(): Promise { log('Sending abnormal session'); updateSession(session, { status: 'abnormal', abnormal_mechanism: 'anr_foreground' }); - log(JSON.stringify(session)); - const envelope = createSessionEnvelope(session, options.dsn, options.sdkMetadata); + // Log the envelope so to aid in testing + log(JSON.stringify(envelope)); + await transport.send(envelope); try { @@ -117,9 +120,10 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): tags: options.staticTags, }; - log(JSON.stringify(event)); - const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata); + // Log the envelope so to aid in testing + log(JSON.stringify(envelope)); + await transport.send(envelope); await transport.flush(2000); @@ -158,8 +162,9 @@ if (options.captureStackTrace) { // copy the frames const callFrames = [...event.params.callFrames]; + const getModuleName = options.appRootPath ? createGetModuleFromFilename(options.appRootPath) : () => undefined; const stackFrames = callFrames.map(frame => - callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), () => undefined), + callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), getModuleName), ); // Evaluate a script in the currently paused context diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index d0243d7e3985..eb46b58adf74 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -8,6 +8,8 @@ const INTEGRATION_NAME = 'Console'; const consoleIntegration = (() => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client) { addConsoleInstrumentationHandler(({ args, level }) => { if (getClient() !== client) { diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts index 2b16ec6a4527..461f85297e4b 100644 --- a/packages/node/src/integrations/context.ts +++ b/packages/node/src/integrations/context.ts @@ -98,6 +98,8 @@ const contextIntegration = ((options: ContextOptions = {}) => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event) { return addContext(event); }, diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node/src/integrations/contextlines.ts index 0a961ddbc259..4f7ab81f063e 100644 --- a/packages/node/src/integrations/contextlines.ts +++ b/packages/node/src/integrations/contextlines.ts @@ -40,6 +40,8 @@ const contextLinesIntegration = ((options: ContextLinesOptions = {}) => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event) { return addSourceContext(event, contextLines); }, diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index 409470680a73..42335f7c4ce5 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -5,6 +5,7 @@ import { convertIntegrationFnToClass, getActiveTransaction, getCurrentScope, + getDynamicSamplingContextFromSpan, spanToTraceHeader, startTransaction, } from '@sentry/core'; @@ -45,6 +46,7 @@ export const hapiErrorPlugin = { const server = serverArg as unknown as Server; server.events.on('request', (request, event) => { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (request.response && isBoomObject(request.response)) { @@ -85,12 +87,14 @@ export const hapiTracingPlugin = { }, ); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); return h.continue; }); server.ext('onPreResponse', (request, h) => { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (request.response && isResponseObject(request.response) && transaction) { @@ -98,7 +102,7 @@ export const hapiTracingPlugin = { response.header('sentry-trace', spanToTraceHeader(transaction)); const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader( - transaction.getDynamicSamplingContext(), + getDynamicSamplingContextFromSpan(transaction), ); if (dynamicSamplingContext) { @@ -110,6 +114,7 @@ export const hapiTracingPlugin = { }); server.ext('onPostHandler', (request, h) => { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (request.response && isResponseObject(request.response) && transaction) { diff --git a/packages/node/src/integrations/hapi/types.ts b/packages/node/src/integrations/hapi/types.ts index d74c171ef441..a650667fe362 100644 --- a/packages/node/src/integrations/hapi/types.ts +++ b/packages/node/src/integrations/hapi/types.ts @@ -3,6 +3,7 @@ /* eslint-disable @typescript-eslint/unified-signatures */ /* eslint-disable @typescript-eslint/no-empty-interface */ /* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Vendored and simplified from: // - @types/hapi__hapi diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 61046eb8f38d..e3f6d164d991 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,9 +1,18 @@ import type * as http from 'http'; import type * as https from 'https'; import type { Hub } from '@sentry/core'; -import { spanToTraceHeader } from '@sentry/core'; -import { addBreadcrumb, getClient, getCurrentScope } from '@sentry/core'; -import { getCurrentHub, getDynamicSamplingContextFromClient, isSentryRequestUrl } from '@sentry/core'; +import { + addBreadcrumb, + getActiveSpan, + getClient, + getCurrentHub, + getCurrentScope, + getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, + isSentryRequestUrl, + spanToJSON, + spanToTraceHeader, +} from '@sentry/core'; import type { DynamicSamplingContext, EventProcessor, @@ -105,6 +114,7 @@ export class Http implements Integration { return; } + // eslint-disable-next-line deprecation/deprecation const clientOptions = setupOnceGetCurrentHub().getClient()?.getOptions(); // Do not auto-instrument for other instrumenter @@ -211,6 +221,7 @@ function _createWrappedRequestMethodFactory( req: http.ClientRequest, res?: http.IncomingMessage, ): void { + // eslint-disable-next-line deprecation/deprecation if (!getCurrentHub().getIntegration(Http)) { return; } @@ -246,12 +257,13 @@ function _createWrappedRequestMethodFactory( } const scope = getCurrentScope(); - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const data = getRequestSpanData(requestUrl, requestOptions); const requestSpan = shouldCreateSpan(rawRequestUrl) - ? parentSpan?.startChild({ + ? // eslint-disable-next-line deprecation/deprecation + parentSpan?.startChild({ op: 'http.client', origin: 'auto.http.node.http', description: `${data['http.method']} ${data.url}`, @@ -262,7 +274,7 @@ function _createWrappedRequestMethodFactory( if (shouldAttachTraceData(rawRequestUrl)) { if (requestSpan) { const sentryTraceHeader = spanToTraceHeader(requestSpan); - const dynamicSamplingContext = requestSpan?.transaction?.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan); addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext); } else { const client = getClient(); @@ -292,7 +304,9 @@ function _createWrappedRequestMethodFactory( if (res.statusCode) { requestSpan.setHttpStatus(res.statusCode); } - requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); + requestSpan.updateName( + cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '', + ); requestSpan.end(); } }) @@ -305,7 +319,9 @@ function _createWrappedRequestMethodFactory( } if (requestSpan) { requestSpan.setHttpStatus(500); - requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); + requestSpan.updateName( + cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '', + ); requestSpan.end(); } }); diff --git a/packages/node/src/integrations/local-variables/index.ts b/packages/node/src/integrations/local-variables/index.ts index 970eaea52719..708b4b41ea24 100644 --- a/packages/node/src/integrations/local-variables/index.ts +++ b/packages/node/src/integrations/local-variables/index.ts @@ -1,21 +1,6 @@ -import { convertIntegrationFnToClass } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; -import { NODE_VERSION } from '../../nodeVersion'; -import type { Options } from './common'; -import { localVariablesAsync } from './local-variables-async'; -import { localVariablesSync } from './local-variables-sync'; - -const INTEGRATION_NAME = 'LocalVariables'; - -/** - * Adds local variables to exception frames - */ -const localVariables: IntegrationFn = (options: Options = {}) => { - return NODE_VERSION.major < 19 ? localVariablesSync(options) : localVariablesAsync(options); -}; +import { LocalVariablesSync } from './local-variables-sync'; /** * Adds local variables to exception frames */ -// eslint-disable-next-line deprecation/deprecation -export const LocalVariables = convertIntegrationFnToClass(INTEGRATION_NAME, localVariables); +export const LocalVariables = LocalVariablesSync; diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node/src/integrations/local-variables/local-variables-async.ts index c3072c6c3f11..360d553e48a2 100644 --- a/packages/node/src/integrations/local-variables/local-variables-async.ts +++ b/packages/node/src/integrations/local-variables/local-variables-async.ts @@ -214,6 +214,8 @@ export const localVariablesAsync: IntegrationFn = (options: Options = {}) => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client: NodeClient) { const clientOptions = client.getOptions(); diff --git a/packages/node/src/integrations/local-variables/local-variables-sync.ts b/packages/node/src/integrations/local-variables/local-variables-sync.ts index d2b988cca1e9..45d17748ec78 100644 --- a/packages/node/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node/src/integrations/local-variables/local-variables-sync.ts @@ -208,7 +208,7 @@ function tryNewAsyncSession(): AsyncSession | undefined { } } -const INTEGRATION_NAME = 'LocalVariablesSync'; +const INTEGRATION_NAME = 'LocalVariables'; /** * Adds local variables to exception frames @@ -326,6 +326,8 @@ export const localVariablesSync: IntegrationFn = ( return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client: NodeClient) { const clientOptions = client.getOptions(); diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index ad6549dd3b3b..85bb792f60b2 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -79,6 +79,8 @@ function _getModules(): { [key: string]: string } { const modulesIntegration = (() => { return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function processEvent(event) { event.modules = { ...event.modules, diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index e9c724f89e7f..bdee16d3e953 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -48,6 +48,8 @@ const onUncaughtExceptionIntegration = ((options: Partial = {} return { name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client) { if (typeof process === 'object' && process.env && process.env.NODE_ENV !== 'development') { logger.warn("[Spotlight] It seems you're not in dev mode. Do you really want to have Spotlight enabled?"); diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 117ef602ac38..ef875891b3ef 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,9 +1,10 @@ import { addBreadcrumb, + getActiveSpan, getClient, - getCurrentHub, getCurrentScope, getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, isSentryRequestUrl, spanToTraceHeader, } from '@sentry/core'; @@ -136,8 +137,8 @@ export class Undici implements Integration { } private _onRequestCreate = (message: unknown): void => { - const hub = getCurrentHub(); - if (!hub.getIntegration(Undici)) { + // eslint-disable-next-line deprecation/deprecation + if (!getClient()?.getIntegration(Undici)) { return; } @@ -156,8 +157,7 @@ export class Undici implements Integration { const clientOptions = client.getOptions(); const scope = getCurrentScope(); - - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const span = this._shouldCreateSpan(stringUrl) ? createRequestSpan(parentSpan, request, stringUrl) : undefined; if (span) { @@ -181,7 +181,7 @@ export class Undici implements Integration { if (shouldAttachTraceData(stringUrl)) { if (span) { - const dynamicSamplingContext = span?.transaction?.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); setHeadersOnRequest(request, spanToTraceHeader(span), sentryBaggageHeader); @@ -196,8 +196,8 @@ export class Undici implements Integration { }; private _onRequestEnd = (message: unknown): void => { - const hub = getCurrentHub(); - if (!hub.getIntegration(Undici)) { + // eslint-disable-next-line deprecation/deprecation + if (!getClient()?.getIntegration(Undici)) { return; } @@ -236,8 +236,8 @@ export class Undici implements Integration { }; private _onRequestError = (message: unknown): void => { - const hub = getCurrentHub(); - if (!hub.getIntegration(Undici)) { + // eslint-disable-next-line deprecation/deprecation + if (!getClient()?.getIntegration(Undici)) { return; } @@ -310,6 +310,7 @@ function createRequestSpan( if (url.hash) { data['http.fragment'] = url.hash; } + // eslint-disable-next-line deprecation/deprecation return activeSpan?.startChild({ op: 'http.client', origin: 'auto.http.node.undici', diff --git a/packages/node/src/module.ts b/packages/node/src/module.ts index 73364493ddb2..d873bf9b2f2e 100644 --- a/packages/node/src/module.ts +++ b/packages/node/src/module.ts @@ -8,62 +8,50 @@ function normalizeWindowsPath(path: string): string { .replace(/\\/g, '/'); // replace all `\` instances with `/` } -// We cache this so we don't have to recompute it -let basePath: string | undefined; - -function getBasePath(): string { - if (!basePath) { - const baseDir = - require && require.main && require.main.filename ? dirname(require.main.filename) : global.process.cwd(); - basePath = `${baseDir}/`; - } - - return basePath; -} - -/** Gets the module from a filename */ -export function getModuleFromFilename( - filename: string | undefined, - basePath: string = getBasePath(), +/** Creates a function that gets the module name from a filename */ +export function createGetModuleFromFilename( + basePath: string = process.argv[1] ? dirname(process.argv[1]) : process.cwd(), isWindows: boolean = sep === '\\', -): string | undefined { - if (!filename) { - return; - } - +): (filename: string | undefined) => string | undefined { const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath; - const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename; - // eslint-disable-next-line prefer-const - let { dir, base: file, ext } = posix.parse(normalizedFilename); + return (filename: string | undefined) => { + if (!filename) { + return; + } - if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { - file = file.slice(0, ext.length * -1); - } + const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename; - if (!dir) { - // No dirname whatsoever - dir = '.'; - } + // eslint-disable-next-line prefer-const + let { dir, base: file, ext } = posix.parse(normalizedFilename); - let n = dir.lastIndexOf('/node_modules'); - if (n > -1) { - return `${dir.slice(n + 14).replace(/\//g, '.')}:${file}`; - } + if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { + file = file.slice(0, ext.length * -1); + } - // Let's see if it's a part of the main module - // To be a part of main module, it has to share the same base - n = `${dir}/`.lastIndexOf(normalizedBase, 0); - if (n === 0) { - let moduleName = dir.slice(normalizedBase.length).replace(/\//g, '.'); + if (!dir) { + // No dirname whatsoever + dir = '.'; + } - if (moduleName) { - moduleName += ':'; + const n = dir.lastIndexOf('/node_modules'); + if (n > -1) { + return `${dir.slice(n + 14).replace(/\//g, '.')}:${file}`; } - moduleName += file; - return moduleName; - } + // Let's see if it's a part of the main module + // To be a part of main module, it has to share the same base + if (dir.startsWith(normalizedBase)) { + let moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.'); + + if (moduleName) { + moduleName += ':'; + } + moduleName += file; + + return moduleName; + } - return file; + return file; + }; } diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index c9a1c108dfdd..00091f6192c7 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -1,12 +1,14 @@ /* eslint-disable max-lines */ import { Integrations as CoreIntegrations, + endSession, getClient, - getCurrentHub, getCurrentScope, getIntegrationsToSetup, + getIsolationScope, getMainCarrier, initAndBind, + startSession, } from '@sentry/core'; import type { SessionStatus, StackParser } from '@sentry/types'; import { @@ -32,7 +34,7 @@ import { Spotlight, Undici, } from './integrations'; -import { getModuleFromFilename } from './module'; +import { createGetModuleFromFilename } from './module'; import { makeNodeTransport } from './transports'; import type { NodeClientOptions, NodeOptions } from './types'; @@ -181,7 +183,11 @@ export function init(options: NodeOptions = {}): void { const client = getClient(); if (client && client.addIntegration) { // force integrations to be setup even if no DSN was set - client.setupIntegrations(true); + // If they have already been added before, they will be ignored anyhow + const integrations = client.getOptions().integrations; + for (const integration of integrations) { + client.addIntegration(integration); + } client.addIntegration( new Spotlight({ sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined }), ); @@ -238,26 +244,27 @@ export function getSentryRelease(fallback?: string): string | undefined { } /** Node.js stack parser */ -export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(getModuleFromFilename)); +export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(createGetModuleFromFilename())); /** * Enable automatic Session Tracking for the node process. */ function startSessionTracking(): void { - const hub = getCurrentHub(); - hub.startSession(); + startSession(); // Emitted in the case of healthy sessions, error of `mechanism.handled: true` and unhandledrejections because // The 'beforeExit' event is not emitted for conditions causing explicit termination, // such as calling process.exit() or uncaught exceptions. // Ref: https://nodejs.org/api/process.html#process_event_beforeexit process.on('beforeExit', () => { - const session = hub.getScope().getSession(); + const session = getIsolationScope().getSession(); const terminalStates: SessionStatus[] = ['exited', 'crashed']; // Only call endSession, if the Session exists on Scope and SessionStatus is not a // Terminal Status i.e. Exited or Crashed because // "When a session is moved away from ok it must not be updated anymore." // Ref: https://develop.sentry.dev/sdk/sessions/ - if (session && !terminalStates.includes(session.status)) hub.endSession(); + if (session && !terminalStates.includes(session.status)) { + endSession(); + } }); } diff --git a/packages/node/test/async/domain.test.ts b/packages/node/test/async/domain.test.ts index 95574d48eb35..96f19386e63f 100644 --- a/packages/node/test/async/domain.test.ts +++ b/packages/node/test/async/domain.test.ts @@ -1,137 +1,208 @@ import type { Hub } from '@sentry/core'; +import { getIsolationScope, withIsolationScope } from '@sentry/core'; import { getCurrentHub, runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core'; import { setDomainAsyncContextStrategy } from '../../src/async/domain'; -describe('domains', () => { - afterAll(() => { +describe('setDomainAsyncContextStrategy()', () => { + afterEach(() => { // clear the strategy setAsyncContextStrategy(undefined); }); - test('hub scope inheritance', () => { - setDomainAsyncContextStrategy(); + describe('with withIsolationScope()', () => { + it('forks the isolation scope (creating a new one)', done => { + expect.assertions(7); + setDomainAsyncContextStrategy(); - const globalHub = getCurrentHub(); - globalHub.setExtra('a', 'b'); + const topLevelIsolationScope = getIsolationScope(); + topLevelIsolationScope.setTag('val1', true); - runWithAsyncContext(() => { - const hub1 = getCurrentHub(); - expect(hub1).toEqual(globalHub); + withIsolationScope(isolationScope1 => { + expect(isolationScope1).not.toBe(topLevelIsolationScope); + expect(isolationScope1.getScopeData().tags['val1']).toBe(true); + isolationScope1.setTag('val2', true); + topLevelIsolationScope.setTag('val3', true); - hub1.setExtra('c', 'd'); - expect(hub1).not.toEqual(globalHub); + withIsolationScope(isolationScope2 => { + expect(isolationScope2).not.toBe(isolationScope1); + expect(isolationScope2).not.toBe(topLevelIsolationScope); + expect(isolationScope2.getScopeData().tags['val1']).toBe(true); + expect(isolationScope2.getScopeData().tags['val2']).toBe(true); + expect(isolationScope2.getScopeData().tags['val3']).toBeUndefined(); - runWithAsyncContext(() => { - const hub2 = getCurrentHub(); - expect(hub2).toEqual(hub1); - expect(hub2).not.toEqual(globalHub); - - hub2.setExtra('e', 'f'); - expect(hub2).not.toEqual(hub1); + done(); + }); }); }); - }); - test('async hub scope inheritance', async () => { - setDomainAsyncContextStrategy(); + it('correctly keeps track of isolation scope across asynchronous operations', done => { + expect.assertions(7); + setDomainAsyncContextStrategy(); + + const topLevelIsolationScope = getIsolationScope(); + expect(getIsolationScope()).toBe(topLevelIsolationScope); - async function addRandomExtra(hub: Hub, key: string): Promise { - return new Promise(resolve => { + withIsolationScope(isolationScope1 => { setTimeout(() => { - hub.setExtra(key, Math.random()); - resolve(); + expect(getIsolationScope()).toBe(isolationScope1); + + withIsolationScope(isolationScope2 => { + setTimeout(() => { + expect(getIsolationScope()).toBe(isolationScope2); + }, 100); + }); + + setTimeout(() => { + expect(getIsolationScope()).toBe(isolationScope1); + done(); + }, 200); + + expect(getIsolationScope()).toBe(isolationScope1); }, 100); }); - } - const globalHub = getCurrentHub(); - await addRandomExtra(globalHub, 'a'); + setTimeout(() => { + expect(getIsolationScope()).toBe(topLevelIsolationScope); + }, 200); - await runWithAsyncContext(async () => { - const hub1 = getCurrentHub(); - expect(hub1).toEqual(globalHub); + expect(getIsolationScope()).toBe(topLevelIsolationScope); + }); + }); - await addRandomExtra(hub1, 'b'); - expect(hub1).not.toEqual(globalHub); + describe('with runWithAsyncContext()', () => { + test('hub scope inheritance', () => { + setDomainAsyncContextStrategy(); - await runWithAsyncContext(async () => { - const hub2 = getCurrentHub(); - expect(hub2).toEqual(hub1); - expect(hub2).not.toEqual(globalHub); + const globalHub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + globalHub.setExtra('a', 'b'); - await addRandomExtra(hub1, 'c'); - expect(hub2).not.toEqual(hub1); + runWithAsyncContext(() => { + const hub1 = getCurrentHub(); + expect(hub1).toEqual(globalHub); + + // eslint-disable-next-line deprecation/deprecation + hub1.setExtra('c', 'd'); + expect(hub1).not.toEqual(globalHub); + + runWithAsyncContext(() => { + const hub2 = getCurrentHub(); + expect(hub2).toEqual(hub1); + expect(hub2).not.toEqual(globalHub); + + // eslint-disable-next-line deprecation/deprecation + hub2.setExtra('e', 'f'); + expect(hub2).not.toEqual(hub1); + }); }); }); - }); - test('hub single instance', () => { - setDomainAsyncContextStrategy(); + test('async hub scope inheritance', async () => { + setDomainAsyncContextStrategy(); - runWithAsyncContext(() => { - const hub = getCurrentHub(); - expect(hub).toBe(getCurrentHub()); + async function addRandomExtra(hub: Hub, key: string): Promise { + return new Promise(resolve => { + setTimeout(() => { + // eslint-disable-next-line deprecation/deprecation + hub.setExtra(key, Math.random()); + resolve(); + }, 100); + }); + } + + const globalHub = getCurrentHub(); + await addRandomExtra(globalHub, 'a'); + + await runWithAsyncContext(async () => { + const hub1 = getCurrentHub(); + expect(hub1).toEqual(globalHub); + + await addRandomExtra(hub1, 'b'); + expect(hub1).not.toEqual(globalHub); + + await runWithAsyncContext(async () => { + const hub2 = getCurrentHub(); + expect(hub2).toEqual(hub1); + expect(hub2).not.toEqual(globalHub); + + await addRandomExtra(hub1, 'c'); + expect(hub2).not.toEqual(hub1); + }); + }); }); - }); - test('within a domain not reused', () => { - setDomainAsyncContextStrategy(); + test('hub single instance', () => { + setDomainAsyncContextStrategy(); - runWithAsyncContext(() => { - const hub1 = getCurrentHub(); runWithAsyncContext(() => { - const hub2 = getCurrentHub(); - expect(hub1).not.toBe(hub2); + const hub = getCurrentHub(); + expect(hub).toBe(getCurrentHub()); }); }); - }); - test('within a domain reused when requested', () => { - setDomainAsyncContextStrategy(); + test('within a domain not reused', () => { + setDomainAsyncContextStrategy(); - runWithAsyncContext(() => { - const hub1 = getCurrentHub(); - runWithAsyncContext( - () => { + runWithAsyncContext(() => { + const hub1 = getCurrentHub(); + runWithAsyncContext(() => { const hub2 = getCurrentHub(); - expect(hub1).toBe(hub2); - }, - { reuseExisting: true }, - ); + expect(hub1).not.toBe(hub2); + }); + }); }); - }); - - test('concurrent hub contexts', done => { - setDomainAsyncContextStrategy(); - let d1done = false; - let d2done = false; + test('within a domain reused when requested', () => { + setDomainAsyncContextStrategy(); - runWithAsyncContext(() => { - const hub = getCurrentHub(); - hub.getStack().push({ client: 'process' } as any); - expect(hub.getStack()[1]).toEqual({ client: 'process' }); - // Just in case so we don't have to worry which one finishes first - // (although it always should be d2) - setTimeout(() => { - d1done = true; - if (d2done) { - done(); - } - }, 0); + runWithAsyncContext(() => { + const hub1 = getCurrentHub(); + runWithAsyncContext( + () => { + const hub2 = getCurrentHub(); + expect(hub1).toBe(hub2); + }, + { reuseExisting: true }, + ); + }); }); - runWithAsyncContext(() => { - const hub = getCurrentHub(); - hub.getStack().push({ client: 'local' } as any); - expect(hub.getStack()[1]).toEqual({ client: 'local' }); - setTimeout(() => { - d2done = true; - if (d1done) { - done(); - } - }, 0); + test('concurrent hub contexts', done => { + setDomainAsyncContextStrategy(); + + let d1done = false; + let d2done = false; + + runWithAsyncContext(() => { + const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.getStack().push({ client: 'process' } as any); + // eslint-disable-next-line deprecation/deprecation + expect(hub.getStack()[1]).toEqual({ client: 'process' }); + // Just in case so we don't have to worry which one finishes first + // (although it always should be d2) + setTimeout(() => { + d1done = true; + if (d2done) { + done(); + } + }, 0); + }); + + runWithAsyncContext(() => { + const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.getStack().push({ client: 'local' } as any); + // eslint-disable-next-line deprecation/deprecation + expect(hub.getStack()[1]).toEqual({ client: 'local' }); + setTimeout(() => { + d2done = true; + if (d1done) { + done(); + } + }, 0); + }); }); }); }); diff --git a/packages/node/test/async/hooks.test.ts b/packages/node/test/async/hooks.test.ts index ad477e03d477..f42619839043 100644 --- a/packages/node/test/async/hooks.test.ts +++ b/packages/node/test/async/hooks.test.ts @@ -1,148 +1,210 @@ import type { Hub } from '@sentry/core'; +import { getIsolationScope } from '@sentry/core'; +import { withIsolationScope } from '@sentry/core'; import { getCurrentHub, runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core'; import { setHooksAsyncContextStrategy } from '../../src/async/hooks'; import { conditionalTest } from '../utils'; -conditionalTest({ min: 12 })('async_hooks', () => { - afterAll(() => { +conditionalTest({ min: 12 })('setHooksAsyncContextStrategy()', () => { + afterEach(() => { // clear the strategy setAsyncContextStrategy(undefined); }); - test('without strategy hubs should be equal', () => { - runWithAsyncContext(() => { - const hub1 = getCurrentHub(); - runWithAsyncContext(() => { - const hub2 = getCurrentHub(); - expect(hub1).toBe(hub2); + describe('with withIsolationScope()', () => { + it('forks the isolation scope (creating a new one)', done => { + expect.assertions(7); + setHooksAsyncContextStrategy(); + + const topLevelIsolationScope = getIsolationScope(); + topLevelIsolationScope.setTag('val1', true); + + withIsolationScope(isolationScope1 => { + expect(isolationScope1).not.toBe(topLevelIsolationScope); + expect(isolationScope1.getScopeData().tags['val1']).toBe(true); + isolationScope1.setTag('val2', true); + topLevelIsolationScope.setTag('val3', true); + + withIsolationScope(isolationScope2 => { + expect(isolationScope2).not.toBe(isolationScope1); + expect(isolationScope2).not.toBe(topLevelIsolationScope); + expect(isolationScope2.getScopeData().tags['val1']).toBe(true); + expect(isolationScope2.getScopeData().tags['val2']).toBe(true); + expect(isolationScope2.getScopeData().tags['val3']).toBeUndefined(); + + done(); + }); }); }); - }); - test('hub scope inheritance', () => { - setHooksAsyncContextStrategy(); + it('correctly keeps track of isolation scope across asynchronous operations', done => { + expect.assertions(7); + setHooksAsyncContextStrategy(); - const globalHub = getCurrentHub(); - globalHub.setExtra('a', 'b'); + const topLevelIsolationScope = getIsolationScope(); + expect(getIsolationScope()).toBe(topLevelIsolationScope); - runWithAsyncContext(() => { - const hub1 = getCurrentHub(); - expect(hub1).toEqual(globalHub); + withIsolationScope(isolationScope1 => { + setTimeout(() => { + expect(getIsolationScope()).toBe(isolationScope1); - hub1.setExtra('c', 'd'); - expect(hub1).not.toEqual(globalHub); + withIsolationScope(isolationScope2 => { + setTimeout(() => { + expect(getIsolationScope()).toBe(isolationScope2); + }, 100); + }); - runWithAsyncContext(() => { - const hub2 = getCurrentHub(); - expect(hub2).toEqual(hub1); - expect(hub2).not.toEqual(globalHub); + setTimeout(() => { + expect(getIsolationScope()).toBe(isolationScope1); + done(); + }, 200); - hub2.setExtra('e', 'f'); - expect(hub2).not.toEqual(hub1); + expect(getIsolationScope()).toBe(isolationScope1); + }, 100); }); + + setTimeout(() => { + expect(getIsolationScope()).toBe(topLevelIsolationScope); + }, 200); + + expect(getIsolationScope()).toBe(topLevelIsolationScope); }); }); - test('async hub scope inheritance', async () => { - setHooksAsyncContextStrategy(); + describe('with runWithAsyncContext()', () => { + test('hub scope inheritance', () => { + setHooksAsyncContextStrategy(); - async function addRandomExtra(hub: Hub, key: string): Promise { - return new Promise(resolve => { - setTimeout(() => { - hub.setExtra(key, Math.random()); - resolve(); - }, 100); + const globalHub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + globalHub.setExtra('a', 'b'); + + runWithAsyncContext(() => { + const hub1 = getCurrentHub(); + expect(hub1).toEqual(globalHub); + + // eslint-disable-next-line deprecation/deprecation + hub1.setExtra('c', 'd'); + expect(hub1).not.toEqual(globalHub); + + runWithAsyncContext(() => { + const hub2 = getCurrentHub(); + expect(hub2).toEqual(hub1); + expect(hub2).not.toEqual(globalHub); + + // eslint-disable-next-line deprecation/deprecation + hub2.setExtra('e', 'f'); + expect(hub2).not.toEqual(hub1); + }); }); - } + }); - const globalHub = getCurrentHub(); - await addRandomExtra(globalHub, 'a'); + test('async hub scope inheritance', async () => { + setHooksAsyncContextStrategy(); - await runWithAsyncContext(async () => { - const hub1 = getCurrentHub(); - expect(hub1).toEqual(globalHub); + async function addRandomExtra(hub: Hub, key: string): Promise { + return new Promise(resolve => { + setTimeout(() => { + // eslint-disable-next-line deprecation/deprecation + hub.setExtra(key, Math.random()); + resolve(); + }, 100); + }); + } - await addRandomExtra(hub1, 'b'); - expect(hub1).not.toEqual(globalHub); + const globalHub = getCurrentHub(); + await addRandomExtra(globalHub, 'a'); await runWithAsyncContext(async () => { - const hub2 = getCurrentHub(); - expect(hub2).toEqual(hub1); - expect(hub2).not.toEqual(globalHub); + const hub1 = getCurrentHub(); + expect(hub1).toEqual(globalHub); + + await addRandomExtra(hub1, 'b'); + expect(hub1).not.toEqual(globalHub); - await addRandomExtra(hub1, 'c'); - expect(hub2).not.toEqual(hub1); + await runWithAsyncContext(async () => { + const hub2 = getCurrentHub(); + expect(hub2).toEqual(hub1); + expect(hub2).not.toEqual(globalHub); + + await addRandomExtra(hub1, 'c'); + expect(hub2).not.toEqual(hub1); + }); }); }); - }); - test('context single instance', () => { - setHooksAsyncContextStrategy(); + test('context single instance', () => { + setHooksAsyncContextStrategy(); - const globalHub = getCurrentHub(); - runWithAsyncContext(() => { - expect(globalHub).not.toBe(getCurrentHub()); + const globalHub = getCurrentHub(); + runWithAsyncContext(() => { + expect(globalHub).not.toBe(getCurrentHub()); + }); }); - }); - test('context within a context not reused', () => { - setHooksAsyncContextStrategy(); + test('context within a context not reused', () => { + setHooksAsyncContextStrategy(); - runWithAsyncContext(() => { - const hub1 = getCurrentHub(); runWithAsyncContext(() => { - const hub2 = getCurrentHub(); - expect(hub1).not.toBe(hub2); + const hub1 = getCurrentHub(); + runWithAsyncContext(() => { + const hub2 = getCurrentHub(); + expect(hub1).not.toBe(hub2); + }); }); }); - }); - test('context within a context reused when requested', () => { - setHooksAsyncContextStrategy(); + test('context within a context reused when requested', () => { + setHooksAsyncContextStrategy(); - runWithAsyncContext(() => { - const hub1 = getCurrentHub(); - runWithAsyncContext( - () => { - const hub2 = getCurrentHub(); - expect(hub1).toBe(hub2); - }, - { reuseExisting: true }, - ); + runWithAsyncContext(() => { + const hub1 = getCurrentHub(); + runWithAsyncContext( + () => { + const hub2 = getCurrentHub(); + expect(hub1).toBe(hub2); + }, + { reuseExisting: true }, + ); + }); }); - }); - test('concurrent hub contexts', done => { - setHooksAsyncContextStrategy(); + test('concurrent hub contexts', done => { + setHooksAsyncContextStrategy(); - let d1done = false; - let d2done = false; + let d1done = false; + let d2done = false; - runWithAsyncContext(() => { - const hub = getCurrentHub(); - hub.getStack().push({ client: 'process' } as any); - expect(hub.getStack()[1]).toEqual({ client: 'process' }); - // Just in case so we don't have to worry which one finishes first - // (although it always should be d2) - setTimeout(() => { - d1done = true; - if (d2done) { - done(); - } - }, 0); - }); + runWithAsyncContext(() => { + const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.getStack().push({ client: 'process' } as any); + // eslint-disable-next-line deprecation/deprecation + expect(hub.getStack()[1]).toEqual({ client: 'process' }); + // Just in case so we don't have to worry which one finishes first + // (although it always should be d2) + setTimeout(() => { + d1done = true; + if (d2done) { + done(); + } + }, 0); + }); - runWithAsyncContext(() => { - const hub = getCurrentHub(); - hub.getStack().push({ client: 'local' } as any); - expect(hub.getStack()[1]).toEqual({ client: 'local' }); - setTimeout(() => { - d2done = true; - if (d1done) { - done(); - } - }, 0); + runWithAsyncContext(() => { + const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.getStack().push({ client: 'local' } as any); + // eslint-disable-next-line deprecation/deprecation + expect(hub.getStack()[1]).toEqual({ client: 'local' }); + setTimeout(() => { + d2done = true; + if (d1done) { + done(); + } + }, 0); + }); }); }); }); diff --git a/packages/node/test/cron.test.ts b/packages/node/test/cron.test.ts index d37fcf189926..eee6d4a66711 100644 --- a/packages/node/test/cron.test.ts +++ b/packages/node/test/cron.test.ts @@ -78,6 +78,26 @@ describe('cron check-ins', () => { }, }); }); + + test('throws with multiple jobs same name', () => { + const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job'); + + CronJobWithCheckIn.from({ + cronTime: '* * * Jan,Sep Sun', + onTick: () => { + // + }, + }); + + expect(() => { + CronJobWithCheckIn.from({ + cronTime: '* * * Jan,Sep Sun', + onTick: () => { + // + }, + }); + }).toThrowError("A job named 'my-cron-job' has already been scheduled"); + }); }); describe('node-cron', () => { @@ -118,10 +138,76 @@ describe('cron check-ins', () => { const cronWithCheckIn = cron.instrumentNodeCron(nodeCron); expect(() => { + // @ts-expect-error Initially missing name cronWithCheckIn.schedule('* * * * *', () => { // }); }).toThrowError('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.'); }); }); + + describe('node-schedule', () => { + test('calls withMonitor', done => { + expect.assertions(5); + + class NodeScheduleMock { + scheduleJob( + nameOrExpression: string | Date | object, + expressionOrCallback: string | Date | object | (() => void), + callback: () => void, + ): unknown { + expect(nameOrExpression).toBe('my-cron-job'); + expect(expressionOrCallback).toBe('* * * Jan,Sep Sun'); + expect(callback).toBeInstanceOf(Function); + return callback(); + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * Jan,Sep Sun', () => { + expect(withMonitorSpy).toHaveBeenCalledTimes(1); + expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { + schedule: { type: 'crontab', value: '* * * 1,9 0' }, + }); + done(); + }); + }); + + test('throws without crontab string', () => { + class NodeScheduleMock { + scheduleJob(_: string, __: string | Date, ___: () => void): unknown { + return undefined; + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + expect(() => { + scheduleWithCheckIn.scheduleJob('my-cron-job', new Date(), () => { + // + }); + }).toThrowError( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + }); + + test('throws without job name', () => { + class NodeScheduleMock { + scheduleJob(_: string, __: () => void): unknown { + return undefined; + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + expect(() => { + scheduleWithCheckIn.scheduleJob('* * * * *', () => { + // + }); + }).toThrowError( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + }); + }); }); diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 14de421db5f7..574c4addc27b 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -1,7 +1,7 @@ import * as http from 'http'; import type { Hub } from '@sentry/core'; import * as sentryCore from '@sentry/core'; -import { Transaction, setAsyncContextStrategy } from '@sentry/core'; +import { Transaction, getClient, getCurrentScope, setAsyncContextStrategy } from '@sentry/core'; import type { Event, PropagationContext } from '@sentry/types'; import { SentryError } from '@sentry/utils'; @@ -65,7 +65,7 @@ describe('requestHandler', () => { sentryRequestMiddleware(req, res, next); - const scope = sentryCore.getCurrentHub().getScope(); + const scope = getCurrentScope(); expect(scope?.getRequestSession()).toEqual({ status: 'ok' }); }); @@ -79,7 +79,7 @@ describe('requestHandler', () => { sentryRequestMiddleware(req, res, next); - const scope = sentryCore.getCurrentHub().getScope(); + const scope = getCurrentScope(); expect(scope?.getRequestSession()).toBeUndefined(); }); @@ -95,7 +95,7 @@ describe('requestHandler', () => { sentryRequestMiddleware(req, res, next); - const scope = sentryCore.getCurrentHub().getScope(); + const scope = getCurrentScope(); res.emit('finish'); setImmediate(() => { @@ -116,7 +116,7 @@ describe('requestHandler', () => { const captureRequestSession = jest.spyOn(client, '_captureRequestSession'); sentryRequestMiddleware(req, res, next); - const scope = sentryCore.getCurrentHub().getScope(); + const scope = getCurrentScope(); res.emit('finish'); setImmediate(() => { @@ -163,7 +163,7 @@ describe('requestHandler', () => { sentryRequestMiddleware(req, res, next); - const scope = sentryCore.getCurrentHub().getScope(); + const scope = getCurrentScope(); expect((scope as any)._sdkProcessingMetadata).toEqual({ request: req, requestDataOptionsFromExpressHandler: requestHandlerOptions, @@ -191,7 +191,8 @@ describe('tracingHandler', () => { beforeEach(() => { hub = new sentryCore.Hub(new NodeClient(getDefaultNodeClientOptions({ tracesSampleRate: 1.0 }))); - jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + sentryCore.makeMain(hub); + mockAsyncContextStrategy(() => hub); req = { headers, @@ -210,7 +211,7 @@ describe('tracingHandler', () => { function getPropagationContext(): PropagationContext { // @ts-expect-error accesing private property for test - return hub.getScope()._propagationContext; + return getCurrentScope()._propagationContext; } it('creates a transaction when handling a request', () => { @@ -240,7 +241,7 @@ describe('tracingHandler', () => { }); it("doesn't create a transaction if tracing is disabled", () => { - delete hub.getClient()?.getOptions().tracesSampleRate; + delete getClient()?.getOptions().tracesSampleRate; const startTransaction = jest.spyOn(sentryCore, 'startTransaction'); sentryTracingMiddleware(req, res, next); @@ -340,7 +341,8 @@ describe('tracingHandler', () => { sentryTracingMiddleware(req, res, next); - const transaction = sentryCore.getCurrentHub().getScope().getTransaction(); + // eslint-disable-next-line deprecation/deprecation + const transaction = getCurrentScope().getTransaction(); expect(transaction).toBeDefined(); expect(transaction).toEqual( @@ -360,6 +362,7 @@ describe('tracingHandler', () => { }); it('pulls status code from the response', done => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'mockTransaction' }); jest.spyOn(sentryCore, 'startTransaction').mockReturnValue(transaction as Transaction); const finishTransaction = jest.spyOn(transaction, 'end'); @@ -371,8 +374,11 @@ describe('tracingHandler', () => { setImmediate(() => { expect(finishTransaction).toHaveBeenCalled(); expect(transaction.status).toBe('ok'); + // eslint-disable-next-line deprecation/deprecation expect(transaction.tags).toEqual(expect.objectContaining({ 'http.status_code': '200' })); - expect(transaction.data).toEqual(expect.objectContaining({ 'http.response.status_code': 200 })); + expect(sentryCore.spanToJSON(transaction).data).toEqual( + expect.objectContaining({ 'http.response.status_code': 200 }), + ); done(); }); }); @@ -408,6 +414,7 @@ describe('tracingHandler', () => { }); it('closes the transaction when request processing is done', done => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'mockTransaction' }); jest.spyOn(sentryCore, 'startTransaction').mockReturnValue(transaction as Transaction); const finishTransaction = jest.spyOn(transaction, 'end'); @@ -422,8 +429,10 @@ describe('tracingHandler', () => { }); it('waits to finish transaction until all spans are finished, even though `transaction.end()` is registered on `res.finish` event first', done => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'mockTransaction', sampled: true }); transaction.initSpanRecorder(); + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ description: 'reallyCoolHandler', op: 'middleware', @@ -448,7 +457,7 @@ describe('tracingHandler', () => { expect(finishTransaction).toHaveBeenCalled(); expect(span.endTimestamp).toBeLessThanOrEqual(transaction.endTimestamp!); expect(sentEvent.spans?.length).toEqual(1); - expect(sentEvent.spans?.[0].spanId).toEqual(span.spanId); + expect(sentEvent.spans?.[0].spanContext().spanId).toEqual(span.spanContext().spanId); done(); }); }); @@ -458,12 +467,15 @@ describe('tracingHandler', () => { const hub = new sentryCore.Hub(new NodeClient(options)); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + // eslint-disable-next-line deprecation/deprecation jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); sentryTracingMiddleware(req, res, next); + // eslint-disable-next-line deprecation/deprecation const transaction = sentryCore.getCurrentScope().getTransaction(); + // eslint-disable-next-line deprecation/deprecation expect(transaction?.metadata.request).toEqual(req); }); }); @@ -509,7 +521,7 @@ describe('errorHandler()', () => { // by the`requestHandler`) client.initSessionFlusher(); - const scope = sentryCore.getCurrentHub().getScope(); + const scope = getCurrentScope(); const hub = new sentryCore.Hub(client); jest.spyOn(client, '_captureRequestSession'); @@ -525,7 +537,7 @@ describe('errorHandler()', () => { const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '3.3' }); client = new NodeClient(options); - const scope = sentryCore.getCurrentHub().getScope(); + const scope = getCurrentScope(); const hub = new sentryCore.Hub(client); jest.spyOn(client, '_captureRequestSession'); @@ -552,7 +564,7 @@ describe('errorHandler()', () => { hub.run(() => { scope?.setRequestSession({ status: 'ok' }); sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { - const scope = sentryCore.getCurrentHub().getScope(); + const scope = getCurrentScope(); const requestSession = scope?.getRequestSession(); expect(requestSession).toEqual({ status: 'crashed' }); }); @@ -586,7 +598,9 @@ describe('errorHandler()', () => { // `sentryErrorMiddleware` uses `withScope`, and we need access to the temporary scope it creates, so monkeypatch // `captureException` in order to examine the scope as it exists inside the `withScope` callback + // eslint-disable-next-line deprecation/deprecation hub.captureException = function (this: sentryCore.Hub, _exception: any) { + // eslint-disable-next-line deprecation/deprecation const scope = this.getScope(); expect((scope as any)._sdkProcessingMetadata.request).toEqual(req); } as any; diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 30658128d2b4..a5aa6e5cc699 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -286,8 +286,10 @@ describe('SentryNode', () => { runWithAsyncContext(() => { const hub = getCurrentHub(); hub.bindClient(client); + // eslint-disable-next-line deprecation/deprecation expect(getCurrentHub().getClient()).toBe(client); expect(getClient()).toBe(client); + // eslint-disable-next-line deprecation/deprecation hub.captureEvent({ message: 'test domain' }); }); }); @@ -465,7 +467,7 @@ describe('SentryNode initialization', () => { it('defaults to sentry instrumenter', () => { init({ dsn }); - const instrumenter = (getCurrentHub()?.getClient()?.getOptions() as NodeClientOptions).instrumenter; + const instrumenter = (getClient()?.getOptions() as NodeClientOptions).instrumenter; expect(instrumenter).toEqual('sentry'); }); @@ -473,7 +475,7 @@ describe('SentryNode initialization', () => { it('allows to set instrumenter', () => { init({ dsn, instrumenter: 'otel' }); - const instrumenter = (getCurrentHub()?.getClient()?.getOptions() as NodeClientOptions).instrumenter; + const instrumenter = (getClient()?.getOptions() as NodeClientOptions).instrumenter; expect(instrumenter).toEqual('otel'); }); @@ -484,7 +486,7 @@ describe('SentryNode initialization', () => { process.env.SENTRY_TRACE = '12312012123120121231201212312012-1121201211212012-0'; process.env.SENTRY_BAGGAGE = 'sentry-release=1.0.0,sentry-environment=production'; - getCurrentHub().getScope().clear(); + getCurrentScope().clear(); }); afterEach(() => { @@ -496,7 +498,7 @@ describe('SentryNode initialization', () => { init({ dsn }); // @ts-expect-error accessing private method for test - expect(getCurrentHub().getScope()._propagationContext).toEqual({ + expect(getCurrentScope()._propagationContext).toEqual({ traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', spanId: expect.any(String), @@ -515,7 +517,7 @@ describe('SentryNode initialization', () => { init({ dsn }); // @ts-expect-error accessing private method for test - expect(getCurrentHub().getScope()._propagationContext.traceId).not.toEqual('12312012123120121231201212312012'); + expect(getCurrentScope()._propagationContext.traceId).not.toEqual('12312012123120121231201212312012'); delete process.env.SENTRY_USE_ENVIRONMENT; }, diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 91d5a6c0e20d..609ea80c5075 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -2,7 +2,7 @@ import * as http from 'http'; import * as https from 'https'; import type { Span } from '@sentry/core'; import { Transaction } from '@sentry/core'; -import { startInactiveSpan } from '@sentry/core'; +import { getClient, getCurrentScope, startInactiveSpan } from '@sentry/core'; import * as sentryCore from '@sentry/core'; import { Hub, addTracingExtensions } from '@sentry/core'; import type { TransactionContext } from '@sentry/types'; @@ -22,7 +22,8 @@ const originalHttpRequest = http.request; describe('tracing', () => { afterEach(() => { - sentryCore.getCurrentHub().getScope().setSpan(undefined); + // eslint-disable-next-line deprecation/deprecation + sentryCore.getCurrentScope().setSpan(undefined); }); function createTransactionOnScope( @@ -41,14 +42,18 @@ describe('tracing', () => { sentryCore.makeMain(hub); addTracingExtensions(); + // eslint-disable-next-line deprecation/deprecation hub.getScope().setUser({ id: 'uid123', segment: 'segmentA', }); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + // eslint-disable-next-line deprecation/deprecation jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); - jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); + jest.spyOn(sentryCore, 'getClient').mockReturnValue(getClient()); const transaction = startInactiveSpan({ name: 'dogpark', @@ -58,12 +63,13 @@ describe('tracing', () => { expect(transaction).toBeInstanceOf(Transaction); - hub.getScope().setSpan(transaction); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); return transaction; } - function getHub(customOptions: Partial = {}) { + function setupMockHub(customOptions: Partial = {}) { const options = getDefaultNodeClientOptions({ dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', tracesSampleRate: 1.0, @@ -74,8 +80,11 @@ describe('tracing', () => { }); const hub = new Hub(new NodeClient(options)); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + // eslint-disable-next-line deprecation/deprecation jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); - jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); + jest.spyOn(sentryCore, 'getClient').mockReturnValue(getClient()); return hub; } @@ -90,7 +99,7 @@ describe('tracing', () => { expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual('GET http://dogs.are.great/'); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/'); expect(spans[1].op).toEqual('http.client'); }); @@ -104,7 +113,7 @@ describe('tracing', () => { // only the transaction itself should be there expect(spans.length).toEqual(1); - expect((spans[0] as Transaction).name).toEqual('dogpark'); + expect(sentryCore.spanToJSON(spans[0]).description).toEqual('dogpark'); }); it('attaches the sentry-trace header to outgoing non-sentry requests', async () => { @@ -207,7 +216,7 @@ describe('tracing', () => { it('generates and uses propagation context to attach baggage and sentry-trace header', async () => { nock('http://dogs.are.great').get('/').reply(200); - const { traceId } = sentryCore.getCurrentHub().getScope().getPropagationContext(); + const { traceId } = getCurrentScope().getPropagationContext(); const request = http.get('http://dogs.are.great/'); const sentryTraceHeader = request.getHeader('sentry-trace') as string; @@ -228,8 +237,8 @@ describe('tracing', () => { it('uses incoming propagation context to attach baggage and sentry-trace', async () => { nock('http://dogs.are.great').get('/').reply(200); - const hub = getHub(); - hub.getScope().setPropagationContext({ + setupMockHub(); + getCurrentScope().setPropagationContext({ traceId: '86f39e84263a4de99c326acab3bfe3bd', spanId: '86f39e84263a4de9', sampled: true, @@ -292,12 +301,15 @@ describe('tracing', () => { expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual('GET http://dogs.are.great/spaniel'); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/spaniel'); expect(spans[1].op).toEqual('http.client'); - expect(spans[1].data['http.method']).toEqual('GET'); - expect(spans[1].data.url).toEqual('http://dogs.are.great/spaniel'); - expect(spans[1].data['http.query']).toEqual('tail=wag&cute=true'); - expect(spans[1].data['http.fragment']).toEqual('learn-more'); + + const spanAttributes = sentryCore.spanToJSON(spans[1]).data || {}; + + expect(spanAttributes['http.method']).toEqual('GET'); + expect(spanAttributes.url).toEqual('http://dogs.are.great/spaniel'); + expect(spanAttributes['http.query']).toEqual('tail=wag&cute=true'); + expect(spanAttributes['http.fragment']).toEqual('learn-more'); }); it('fills in span data from http.RequestOptions object', () => { @@ -310,13 +322,15 @@ describe('tracing', () => { expect(spans.length).toEqual(2); + const spanAttributes = sentryCore.spanToJSON(spans[1]).data || {}; + // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual('GET http://dogs.are.great/spaniel'); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/spaniel'); expect(spans[1].op).toEqual('http.client'); - expect(spans[1].data['http.method']).toEqual('GET'); - expect(spans[1].data.url).toEqual('http://dogs.are.great/spaniel'); - expect(spans[1].data['http.query']).toEqual('tail=wag&cute=true'); - expect(spans[1].data['http.fragment']).toEqual('learn-more'); + expect(spanAttributes['http.method']).toEqual('GET'); + expect(spanAttributes.url).toEqual('http://dogs.are.great/spaniel'); + expect(spanAttributes['http.query']).toEqual('tail=wag&cute=true'); + expect(spanAttributes['http.fragment']).toEqual('learn-more'); }); it.each([ @@ -336,7 +350,7 @@ describe('tracing', () => { expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual(`GET http://${redactedAuth}dogs.are.great/`); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual(`GET http://${redactedAuth}dogs.are.great/`); }); describe('Tracing options', () => { @@ -360,8 +374,11 @@ describe('tracing', () => { const hub = new Hub(); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + // eslint-disable-next-line deprecation/deprecation jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); - jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); + jest.spyOn(sentryCore, 'getClient').mockReturnValue(getClient()); const client = new NodeClient(options); jest.spyOn(hub, 'getClient').mockImplementation(() => client); @@ -370,10 +387,11 @@ describe('tracing', () => { return hub; } - function createTransactionAndPutOnScope(hub: Hub) { + function createTransactionAndPutOnScope() { addTracingExtensions(); const transaction = startInactiveSpan({ name: 'dogpark' }); - hub.getScope().setSpan(transaction); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); return transaction; } @@ -387,15 +405,18 @@ describe('tracing', () => { const hub = createHub({ shouldCreateSpanForRequest: () => false }); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + // eslint-disable-next-line deprecation/deprecation jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); - jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); + jest.spyOn(sentryCore, 'getClient').mockReturnValue(getClient()); httpIntegration.setupOnce( () => undefined, () => hub, ); - const transaction = createTransactionAndPutOnScope(hub); + const transaction = createTransactionAndPutOnScope(); const spans = (transaction as unknown as Span).spanRecorder?.spans as Span[]; const request = http.get(url); @@ -408,7 +429,7 @@ describe('tracing', () => { expect(request.getHeader('sentry-trace')).toBeDefined(); expect(request.getHeader('baggage')).toBeDefined(); - const propagationContext = hub.getScope().getPropagationContext(); + const propagationContext = getCurrentScope().getPropagationContext(); expect((request.getHeader('sentry-trace') as string).includes(propagationContext.traceId)).toBe(true); expect(request.getHeader('baggage')).toEqual( @@ -440,7 +461,7 @@ describe('tracing', () => { () => hub, ); - createTransactionAndPutOnScope(hub); + createTransactionAndPutOnScope(); const request = http.get(url); @@ -472,7 +493,7 @@ describe('tracing', () => { () => hub, ); - createTransactionAndPutOnScope(hub); + createTransactionAndPutOnScope(); const request = http.get(url); @@ -496,15 +517,18 @@ describe('tracing', () => { const hub = createHub(); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + // eslint-disable-next-line deprecation/deprecation jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); - jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); + jest.spyOn(sentryCore, 'getClient').mockReturnValue(getClient()); httpIntegration.setupOnce( () => undefined, () => hub, ); - const transaction = createTransactionAndPutOnScope(hub); + const transaction = createTransactionAndPutOnScope(); const spans = (transaction as unknown as Span).spanRecorder?.spans as Span[]; const request = http.get(url); @@ -517,7 +541,7 @@ describe('tracing', () => { expect(request.getHeader('sentry-trace')).toBeDefined(); expect(request.getHeader('baggage')).toBeDefined(); - const propagationContext = hub.getScope().getPropagationContext(); + const propagationContext = getCurrentScope().getPropagationContext(); expect((request.getHeader('sentry-trace') as string).includes(propagationContext.traceId)).toBe(true); expect(request.getHeader('baggage')).toEqual( @@ -549,7 +573,7 @@ describe('tracing', () => { () => hub, ); - createTransactionAndPutOnScope(hub); + createTransactionAndPutOnScope(); const request = http.get(url); @@ -581,7 +605,7 @@ describe('tracing', () => { () => hub, ); - createTransactionAndPutOnScope(hub); + createTransactionAndPutOnScope(); const request = http.get(url); @@ -597,6 +621,7 @@ describe('default protocols', () => { function captureBreadcrumb(key: string): Promise { const hub = new Hub(); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + // eslint-disable-next-line deprecation/deprecation jest.spyOn(sentryCore, 'addBreadcrumb').mockImplementation((...rest) => hub.addBreadcrumb(...rest)); let resolve: (value: Breadcrumb | PromiseLike) => void; diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index 94e3ecaea20a..b0fc6094e6a5 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -162,14 +162,14 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, includeLocalVariables: true, - integrations: [localVariables], + integrations: [], }); const client = new NodeClient(options); - client.setupIntegrations(true); + client.addIntegration(localVariables); const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariablesSync'); + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); expect(eventProcessor).toBeDefined(); @@ -253,11 +253,11 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, includeLocalVariables: true, - integrations: [localVariables], + integrations: [], }); const client = new NodeClient(options); - client.setupIntegrations(true); + client.addIntegration(localVariables); await session.runPause(exceptionEvent100Frames); @@ -278,11 +278,11 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, includeLocalVariables: true, - integrations: [localVariables], + integrations: [], }); const client = new NodeClient(options); - client.setupIntegrations(true); + client.addIntegration(localVariables); const nonExceptionEvent = { method: exceptionEvent.method, @@ -299,14 +299,14 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { const localVariables = new LocalVariablesSync({}, session); const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, - integrations: [localVariables], + integrations: [], }); const client = new NodeClient(options); - client.setupIntegrations(true); + client.addIntegration(localVariables); const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariablesSync'); + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); expect(eventProcessor).toBeDefined(); }); @@ -315,14 +315,14 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { const localVariables = new LocalVariablesSync({}, undefined); const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, - integrations: [localVariables], + integrations: [], }); const client = new NodeClient(options); - client.setupIntegrations(true); + client.addIntegration(localVariables); const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariablesSync'); + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); expect(eventProcessor).toBeDefined(); }); @@ -336,11 +336,11 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, includeLocalVariables: true, - integrations: [localVariables], + integrations: [], }); const client = new NodeClient(options); - client.setupIntegrations(true); + client.addIntegration(localVariables); await session.runPause(exceptionEvent); await session.runPause(exceptionEvent); diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts index 1d7f847b2503..124a1504ba79 100644 --- a/packages/node/test/integrations/undici.test.ts +++ b/packages/node/test/integrations/undici.test.ts @@ -1,5 +1,5 @@ import * as http from 'http'; -import { Transaction, startSpan } from '@sentry/core'; +import { Transaction, getActiveSpan, getClient, getCurrentScope, startSpan } from '@sentry/core'; import { spanToTraceHeader } from '@sentry/core'; import { Hub, makeMain, runWithAsyncContext } from '@sentry/core'; import type { fetch as FetchType } from 'undici'; @@ -182,7 +182,7 @@ conditionalTest({ min: 16 })('Undici integration', () => { // ignore } - expect(hub.getScope().getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); }); it('does create a span if `shouldCreateSpanForRequest` is defined', async () => { @@ -221,7 +221,9 @@ conditionalTest({ min: 16 })('Undici integration', () => { expect(requestHeaders['sentry-trace']).toEqual(spanToTraceHeader(span!)); expect(requestHeaders['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${span.traceId},sentry-sample_rate=1,sentry-transaction=test-transaction`, + `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${ + span.spanContext().traceId + },sentry-sample_rate=1,sentry-transaction=test-transaction`, ); }); }); @@ -230,7 +232,7 @@ conditionalTest({ min: 16 })('Undici integration', () => { // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 // eslint-disable-next-line jest/no-disabled-tests it.skip('attaches the sentry trace and baggage headers if there is no active span', async () => { - const scope = hub.getScope(); + const scope = getCurrentScope(); await fetch('http://localhost:18100', { method: 'POST' }); @@ -245,7 +247,7 @@ conditionalTest({ min: 16 })('Undici integration', () => { // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 // eslint-disable-next-line jest/no-disabled-tests it.skip('attaches headers if `shouldCreateSpanForRequest` does not create a span using propagation context', async () => { - const scope = hub.getScope(); + const scope = getCurrentScope(); const propagationContext = scope.getPropagationContext(); await startSpan({ name: 'outer-span' }, async outerSpan => { @@ -415,7 +417,7 @@ function setupTestServer() { function patchUndici(userOptions: Partial): () => void { try { - const undici = hub.getClient()!.getIntegration(Undici); + const undici = getClient()!.getIntegrationByName!('Undici'); // @ts-expect-error need to access private property options = { ...undici._options }; // @ts-expect-error need to access private property @@ -426,7 +428,7 @@ function patchUndici(userOptions: Partial): () => void { return () => { try { - const undici = hub.getClient()!.getIntegration(Undici); + const undici = getClient()!.getIntegrationByName!('Undici'); // @ts-expect-error Need to override readonly property undici!['_options'] = { ...options }; } catch (_) { diff --git a/packages/node/test/module.test.ts b/packages/node/test/module.test.ts index 04a6a95a7888..cdf97834431e 100644 --- a/packages/node/test/module.test.ts +++ b/packages/node/test/module.test.ts @@ -1,40 +1,33 @@ -import { getModuleFromFilename } from '../src/module'; +import { createGetModuleFromFilename } from '../src/module'; -describe('getModuleFromFilename', () => { - test('Windows', () => { - expect( - getModuleFromFilename('C:\\Users\\Tim\\node_modules\\some-dep\\module.js', 'C:\\Users\\Tim\\', true), - ).toEqual('some-dep:module'); +const getModuleFromFilenameWindows = createGetModuleFromFilename('C:\\Users\\Tim', true); +const getModuleFromFilenamePosix = createGetModuleFromFilename('/Users/Tim'); - expect(getModuleFromFilename('C:\\Users\\Tim\\some\\more\\feature.js', 'C:\\Users\\Tim\\', true)).toEqual( - 'some.more:feature', +describe('createGetModuleFromFilename', () => { + test('Windows', () => { + expect(getModuleFromFilenameWindows('C:\\Users\\Tim\\node_modules\\some-dep\\module.js')).toEqual( + 'some-dep:module', ); + expect(getModuleFromFilenameWindows('C:\\Users\\Tim\\some\\more\\feature.js')).toEqual('some.more:feature'); }); test('POSIX', () => { - expect(getModuleFromFilename('/Users/Tim/node_modules/some-dep/module.js', '/Users/Tim/')).toEqual( - 'some-dep:module', - ); - - expect(getModuleFromFilename('/Users/Tim/some/more/feature.js', '/Users/Tim/')).toEqual('some.more:feature'); - expect(getModuleFromFilename('/Users/Tim/main.js', '/Users/Tim/')).toEqual('main'); + expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.js')).toEqual('some-dep:module'); + expect(getModuleFromFilenamePosix('/Users/Tim/some/more/feature.js')).toEqual('some.more:feature'); + expect(getModuleFromFilenamePosix('/Users/Tim/main.js')).toEqual('main'); }); test('.mjs', () => { - expect(getModuleFromFilename('/Users/Tim/node_modules/some-dep/module.mjs', '/Users/Tim/')).toEqual( - 'some-dep:module', - ); + expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.mjs')).toEqual('some-dep:module'); }); test('.cjs', () => { - expect(getModuleFromFilename('/Users/Tim/node_modules/some-dep/module.cjs', '/Users/Tim/')).toEqual( - 'some-dep:module', - ); + expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.cjs')).toEqual('some-dep:module'); }); test('node internal', () => { - expect(getModuleFromFilename('node.js', '/Users/Tim/')).toEqual('node'); - expect(getModuleFromFilename('node:internal/process/task_queues', '/Users/Tim/')).toEqual('task_queues'); - expect(getModuleFromFilename('node:internal/timers', '/Users/Tim/')).toEqual('timers'); + expect(getModuleFromFilenamePosix('node.js')).toEqual('node'); + expect(getModuleFromFilenamePosix('node:internal/process/task_queues')).toEqual('task_queues'); + expect(getModuleFromFilenamePosix('node:internal/timers')).toEqual('timers'); }); }); diff --git a/packages/node/test/stacktrace.test.ts b/packages/node/test/stacktrace.test.ts index 5b0f6fc52e25..edd62c81d8e5 100644 --- a/packages/node/test/stacktrace.test.ts +++ b/packages/node/test/stacktrace.test.ts @@ -354,6 +354,63 @@ describe('Stack parsing', () => { ]); }); + test('parses with async frames Windows', () => { + // https://github.com/getsentry/sentry-javascript/issues/4692#issuecomment-1063835795 + const err = new Error(); + err.stack = + 'Error: Client request error\n' + + ' at Object.httpRequestError (file:///C:/code/node_modules/@waroncancer/gaia/lib/error/error-factory.js:17:73)\n' + + ' at Object.run (file:///C:/code/node_modules/@waroncancer/gaia/lib/http-client/http-client.js:81:36)\n' + + ' at processTicksAndRejections (node:internal/process/task_queues:96:5)\n' + + ' at async Object.send (file:///C:/code/lib/post-created/send-post-created-notification-module.js:17:27)\n' + + ' at async each (file:///C:/code/lib/process-post-events-module.js:14:21)\n'; + + const frames = parseStackFrames(stackParser, err); + + expect(frames).toEqual([ + { + filename: 'C:/code/lib/process-post-events-module.js', + module: 'process-post-events-module', + function: 'each', + lineno: 14, + colno: 21, + in_app: true, + }, + { + filename: 'C:/code/lib/post-created/send-post-created-notification-module.js', + module: 'send-post-created-notification-module', + function: 'Object.send', + lineno: 17, + colno: 27, + in_app: true, + }, + { + filename: 'node:internal/process/task_queues', + module: 'task_queues', + function: 'processTicksAndRejections', + lineno: 96, + colno: 5, + in_app: false, + }, + { + filename: 'C:/code/node_modules/@waroncancer/gaia/lib/http-client/http-client.js', + module: '@waroncancer.gaia.lib.http-client:http-client', + function: 'Object.run', + lineno: 81, + colno: 36, + in_app: false, + }, + { + filename: 'C:/code/node_modules/@waroncancer/gaia/lib/error/error-factory.js', + module: '@waroncancer.gaia.lib.error:error-factory', + function: 'Object.httpRequestError', + lineno: 17, + colno: 73, + in_app: false, + }, + ]); + }); + test('parses with colons in paths', () => { const err = new Error(); err.stack = diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index fdd57df229f1..b6fe54119788 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry-node", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for OpenTelemetry Node.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry-node", @@ -29,9 +29,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "peerDependencies": { "@opentelemetry/api": "1.x", @@ -45,7 +45,7 @@ "@opentelemetry/sdk-trace-base": "^1.17.1", "@opentelemetry/sdk-trace-node": "^1.17.1", "@opentelemetry/semantic-conventions": "^1.17.1", - "@sentry/node": "7.92.0" + "@sentry/node": "7.93.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/opentelemetry-node/src/propagator.ts b/packages/opentelemetry-node/src/propagator.ts index ce0f295ce720..ead43afdb8f5 100644 --- a/packages/opentelemetry-node/src/propagator.ts +++ b/packages/opentelemetry-node/src/propagator.ts @@ -1,7 +1,7 @@ import type { Baggage, Context, TextMapGetter, TextMapSetter } from '@opentelemetry/api'; import { TraceFlags, isSpanContextValid, propagation, trace } from '@opentelemetry/api'; import { W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; -import { spanToTraceHeader } from '@sentry/core'; +import { getDynamicSamplingContextFromSpan, getRootSpan, spanToTraceHeader } from '@sentry/core'; import { SENTRY_BAGGAGE_KEY_PREFIX, baggageHeaderToDynamicSamplingContext, @@ -35,8 +35,8 @@ export class SentryPropagator extends W3CBaggagePropagator { if (span) { setter.set(carrier, SENTRY_TRACE_HEADER, spanToTraceHeader(span)); - if (span.transaction) { - const dynamicSamplingContext = span.transaction.getDynamicSamplingContext(); + if (getRootSpan(span)) { + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); baggage = Object.entries(dynamicSamplingContext).reduce((b, [dscKey, dscValue]) => { if (dscValue) { return b.setEntry(`${SENTRY_BAGGAGE_KEY_PREFIX}${dscKey}`, { value: dscValue }); diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 5b1a1684c9cf..dcec2ebeef43 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -2,7 +2,14 @@ import type { Context } from '@opentelemetry/api'; import { SpanKind, context, trace } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import type { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { Transaction, addEventProcessor, addTracingExtensions, getClient, getCurrentHub } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + Transaction, + addEventProcessor, + addTracingExtensions, + getClient, + getCurrentHub, +} from '@sentry/core'; import type { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -56,6 +63,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { const sentryParentSpan = otelParentSpanId && getSentrySpan(otelParentSpanId); if (sentryParentSpan) { + // eslint-disable-next-line deprecation/deprecation const sentryChildSpan = sentryParentSpan.startChild({ description: otelSpan.name, instrumenter: 'otel', @@ -186,39 +194,35 @@ function updateSpanWithOtelData(sentrySpan: SentrySpan, otelSpan: OtelSpan): voi const { op, description, data } = parseOtelSpanDescription(otelSpan); sentrySpan.setStatus(mapOtelStatus(otelSpan)); - sentrySpan.setData('otel.kind', SpanKind[kind]); - - const allData = { ...attributes, ...data }; - Object.keys(allData).forEach(prop => { - const value = allData[prop]; - sentrySpan.setData(prop, value); - }); + const allData = { + ...attributes, + ...data, + 'otel.kind': SpanKind[kind], + }; + sentrySpan.setAttributes(allData); sentrySpan.op = op; - sentrySpan.description = description; + sentrySpan.updateName(description); } function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelSpan): void { const { op, description, source, data } = parseOtelSpanDescription(otelSpan); + // eslint-disable-next-line deprecation/deprecation transaction.setContext('otel', { attributes: otelSpan.attributes, resource: otelSpan.resource.attributes, }); const allData = data || {}; - - Object.keys(allData).forEach(prop => { - const value = allData[prop]; - transaction.setData(prop, value); - }); + transaction.setAttributes(allData); transaction.setStatus(mapOtelStatus(otelSpan)); transaction.op = op; transaction.updateName(description); - transaction.setMetadata({ source }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } function convertOtelTimeToSeconds([seconds, nano]: [number, number]): number { diff --git a/packages/opentelemetry-node/src/utils/captureExceptionForTimedEvent.ts b/packages/opentelemetry-node/src/utils/captureExceptionForTimedEvent.ts index 3dde27e49e9f..dcf02b671044 100644 --- a/packages/opentelemetry-node/src/utils/captureExceptionForTimedEvent.ts +++ b/packages/opentelemetry-node/src/utils/captureExceptionForTimedEvent.ts @@ -35,6 +35,7 @@ export function maybeCaptureExceptionForTimedEvent(hub: Hub, event: TimedEvent, syntheticError.name = type; } + // eslint-disable-next-line deprecation/deprecation hub.captureException(syntheticError, { captureContext: otelSpan ? { diff --git a/packages/opentelemetry-node/src/utils/spanData.ts b/packages/opentelemetry-node/src/utils/spanData.ts index 1cdbacf74955..5cec7ee0f93f 100644 --- a/packages/opentelemetry-node/src/utils/spanData.ts +++ b/packages/opentelemetry-node/src/utils/spanData.ts @@ -51,6 +51,7 @@ export function addOtelSpanData( if (sentrySpan instanceof Transaction) { if (metadata) { + // eslint-disable-next-line deprecation/deprecation sentrySpan.setMetadata(metadata); } diff --git a/packages/opentelemetry-node/src/utils/spanMap.ts b/packages/opentelemetry-node/src/utils/spanMap.ts index 8fe43222e93a..9cbdba4460ab 100644 --- a/packages/opentelemetry-node/src/utils/spanMap.ts +++ b/packages/opentelemetry-node/src/utils/spanMap.ts @@ -1,3 +1,4 @@ +import { getRootSpan } from '@sentry/core'; import type { Span as SentrySpan } from '@sentry/types'; interface SpanMapEntry { @@ -31,7 +32,7 @@ export function getSentrySpan(spanId: string): SentrySpan | undefined { export function setSentrySpan(spanId: string, sentrySpan: SentrySpan): void { let ref: SpanRefType = SPAN_REF_ROOT; - const rootSpanId = sentrySpan.transaction?.spanId; + const rootSpanId = getRootSpan(sentrySpan)?.spanContext().spanId; if (rootSpanId && rootSpanId !== spanId) { const root = SPAN_MAP.get(rootSpanId); diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index 345cd9c6eceb..22c686320e76 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -60,13 +60,15 @@ describe('SentryPropagator', () => { } function createTransactionAndMaybeSpan(type: PerfType, transactionContext: TransactionContext) { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction(transactionContext, hub); - setSentrySpan(transaction.spanId, transaction); + setSentrySpan(transaction.spanContext().spanId, transaction); if (type === PerfType.Span) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { spanId, ...ctx } = transactionContext; - const span = transaction.startChild({ ...ctx, description: transaction.name }); - setSentrySpan(span.spanId, span); + // eslint-disable-next-line deprecation/deprecation + const span = transaction.startChild({ ...ctx, name: transactionContext.name }); + setSentrySpan(span.spanContext().spanId, span); } } diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 69ef554c132c..4f8e52a622ae 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -5,7 +5,15 @@ import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import type { SpanStatusType } from '@sentry/core'; -import { Hub, Span as SentrySpan, Transaction, addTracingExtensions, createTransport, makeMain } from '@sentry/core'; +import { + Hub, + Span as SentrySpan, + Transaction, + addTracingExtensions, + createTransport, + makeMain, + spanToJSON, +} from '@sentry/core'; import { NodeClient } from '@sentry/node'; import { resolvedSyncPromise } from '@sentry/utils'; @@ -81,11 +89,11 @@ describe('SentrySpanProcessor', () => { const sentrySpanTransaction = getSpanForOtelSpan(otelSpan) as Transaction | undefined; expect(sentrySpanTransaction).toBeInstanceOf(Transaction); - expect(sentrySpanTransaction?.name).toBe('GET /users'); + expect(spanToJSON(sentrySpanTransaction!).description).toBe('GET /users'); expect(sentrySpanTransaction?.startTimestamp).toEqual(startTimestampMs / 1000); - expect(sentrySpanTransaction?.traceId).toEqual(otelSpan.spanContext().traceId); + expect(sentrySpanTransaction?.spanContext().traceId).toEqual(otelSpan.spanContext().traceId); expect(sentrySpanTransaction?.parentSpanId).toEqual(otelSpan.parentSpanId); - expect(sentrySpanTransaction?.spanId).toEqual(otelSpan.spanContext().spanId); + expect(sentrySpanTransaction?.spanContext().spanId).toEqual(otelSpan.spanContext().spanId); otelSpan.end(endTime); @@ -109,11 +117,12 @@ describe('SentrySpanProcessor', () => { const sentrySpan = getSpanForOtelSpan(childOtelSpan); expect(sentrySpan).toBeInstanceOf(SentrySpan); - expect(sentrySpan?.description).toBe('SELECT * FROM users;'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users;'); expect(sentrySpan?.startTimestamp).toEqual(startTimestampMs / 1000); - expect(sentrySpan?.spanId).toEqual(childOtelSpan.spanContext().spanId); - expect(sentrySpan?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); + expect(sentrySpan?.spanContext().spanId).toEqual(childOtelSpan.spanContext().spanId); + expect(sentrySpan?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); + // eslint-disable-next-line deprecation/deprecation expect(hub.getScope().getSpan()).toBeUndefined(); child.end(endTime); @@ -149,11 +158,12 @@ describe('SentrySpanProcessor', () => { const sentrySpan = getSpanForOtelSpan(childOtelSpan); expect(sentrySpan).toBeInstanceOf(SentrySpan); expect(sentrySpan).toBeInstanceOf(Transaction); - expect(sentrySpan?.name).toBe('SELECT * FROM users;'); + expect(spanToJSON(sentrySpan!).description).toBe('SELECT * FROM users;'); expect(sentrySpan?.startTimestamp).toEqual(startTimestampMs / 1000); - expect(sentrySpan?.spanId).toEqual(childOtelSpan.spanContext().spanId); + expect(sentrySpan?.spanContext().spanId).toEqual(childOtelSpan.spanContext().spanId); expect(sentrySpan?.parentSpanId).toEqual(parentOtelSpan.spanContext().spanId); + // eslint-disable-next-line deprecation/deprecation expect(hub.getScope().getSpan()).toBeUndefined(); child.end(endTime); @@ -172,7 +182,7 @@ describe('SentrySpanProcessor', () => { const sentrySpanTransaction = getSpanForOtelSpan(parentOtelSpan) as Transaction | undefined; expect(sentrySpanTransaction).toBeInstanceOf(SentrySpan); - expect(sentrySpanTransaction?.name).toBe('GET /users'); + expect(spanToJSON(sentrySpanTransaction!).description).toBe('GET /users'); // Create some parallel, independent spans const span1 = tracer.startSpan('SELECT * FROM users;') as OtelSpan; @@ -183,13 +193,13 @@ describe('SentrySpanProcessor', () => { const sentrySpan2 = getSpanForOtelSpan(span2); const sentrySpan3 = getSpanForOtelSpan(span3); - expect(sentrySpan1?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); - expect(sentrySpan2?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); - expect(sentrySpan3?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); + expect(sentrySpan1?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); + expect(sentrySpan2?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); + expect(sentrySpan3?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); - expect(sentrySpan1?.description).toEqual('SELECT * FROM users;'); - expect(sentrySpan2?.description).toEqual('SELECT * FROM companies;'); - expect(sentrySpan3?.description).toEqual('SELECT * FROM locations;'); + expect(spanToJSON(sentrySpan1!).description).toEqual('SELECT * FROM users;'); + expect(spanToJSON(sentrySpan2!).description).toEqual('SELECT * FROM companies;'); + expect(spanToJSON(sentrySpan3!).description).toEqual('SELECT * FROM locations;'); span1.end(); span2.end(); @@ -245,7 +255,7 @@ describe('SentrySpanProcessor', () => { expect(parentSpan?.endTimestamp).toBeDefined(); expect(childSpan?.endTimestamp).toBeDefined(); expect(parentSpan?.parentSpanId).toBeUndefined(); - expect(childSpan?.parentSpanId).toEqual(parentSpan?.spanId); + expect(childSpan?.parentSpanId).toEqual(parentSpan?.spanContext().spanId); }); }); }); @@ -310,11 +320,11 @@ describe('SentrySpanProcessor', () => { const sentrySpan = getSpanForOtelSpan(child); - expect(sentrySpan?.data).toEqual({}); + expect(spanToJSON(sentrySpan!).data).toEqual(undefined); child.end(); - expect(sentrySpan?.data).toEqual({ + expect(spanToJSON(sentrySpan!).data).toEqual({ 'otel.kind': 'INTERNAL', 'test-attribute': 'test-value', 'test-attribute-2': [1, 2, 3], @@ -449,12 +459,12 @@ describe('SentrySpanProcessor', () => { child.updateName('new name'); expect(sentrySpan?.op).toBe(undefined); - expect(sentrySpan?.description).toBe('SELECT * FROM users;'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users;'); child.end(); expect(sentrySpan?.op).toBe(undefined); - expect(sentrySpan?.description).toBe('new name'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('new name'); parentOtelSpan.end(); }); @@ -508,7 +518,7 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('HTTP GET'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('HTTP GET'); parentOtelSpan.end(); }); @@ -529,8 +539,10 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('GET /my/route/{id}'); - expect(sentrySpan?.data).toEqual({ + const { description, data } = spanToJSON(sentrySpan!); + + expect(description).toBe('GET /my/route/{id}'); + expect(data).toEqual({ 'http.method': 'GET', 'http.route': '/my/route/{id}', 'http.target': '/my/route/123', @@ -557,8 +569,10 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('GET http://example.com/my/route/123'); - expect(sentrySpan?.data).toEqual({ + const { description, data } = spanToJSON(sentrySpan!); + + expect(description).toBe('GET http://example.com/my/route/123'); + expect(data).toEqual({ 'http.method': 'GET', 'http.target': '/my/route/123', 'http.url': 'http://example.com/my/route/123', @@ -584,8 +598,10 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('GET http://example.com/my/route/123'); - expect(sentrySpan?.data).toEqual({ + const { description, data } = spanToJSON(sentrySpan!); + + expect(description).toBe('GET http://example.com/my/route/123'); + expect(data).toEqual({ 'http.method': 'GET', 'http.target': '/my/route/123', 'http.url': 'http://example.com/my/route/123?what=123#myHash', @@ -611,6 +627,7 @@ describe('SentrySpanProcessor', () => { otelSpan.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.transaction?.metadata.source).toBe('url'); }); }); @@ -626,6 +643,7 @@ describe('SentrySpanProcessor', () => { otelSpan.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.transaction?.metadata.source).toBe('route'); }); }); @@ -641,6 +659,7 @@ describe('SentrySpanProcessor', () => { otelSpan.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.transaction?.metadata.source).toBe('route'); }); }); @@ -658,7 +677,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('db'); - expect(sentrySpan?.description).toBe('SELECT * FROM users'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users'); parentOtelSpan.end(); }); @@ -677,7 +696,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('db'); - expect(sentrySpan?.description).toBe('fetch users from DB'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('fetch users from DB'); parentOtelSpan.end(); }); @@ -696,7 +715,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('rpc'); - expect(sentrySpan?.description).toBe('test operation'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); parentOtelSpan.end(); }); @@ -715,7 +734,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('message'); - expect(sentrySpan?.description).toBe('test operation'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); parentOtelSpan.end(); }); @@ -734,7 +753,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('test faas trigger'); - expect(sentrySpan?.description).toBe('test operation'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); parentOtelSpan.end(); }); @@ -750,8 +769,8 @@ describe('SentrySpanProcessor', () => { parentOtelSpan.setAttribute(SemanticAttributes.FAAS_TRIGGER, 'test faas trigger'); parentOtelSpan.end(); - expect(transaction?.op).toBe('test faas trigger'); - expect(transaction?.name).toBe('test operation'); + expect(transaction.op).toBe('test faas trigger'); + expect(spanToJSON(transaction).description).toBe('test operation'); }); }); }); @@ -888,6 +907,7 @@ describe('SentrySpanProcessor', () => { tracer.startActiveSpan('GET /users', parentOtelSpan => { tracer.startActiveSpan('SELECT * FROM users;', child => { + // eslint-disable-next-line deprecation/deprecation hub.captureException(new Error('oh nooooo!')); otelSpan = child as OtelSpan; child.end(); @@ -965,7 +985,9 @@ describe('SentrySpanProcessor', () => { hub = new Hub(client); makeMain(hub); + // eslint-disable-next-line deprecation/deprecation const newHub = new Hub(client, hub.getScope().clone()); + // eslint-disable-next-line deprecation/deprecation newHub.getScope().setTag('foo', 'bar'); const tracer = provider.getTracer('default'); diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 75d177255f0d..3d4ac9286287 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry utilities for OpenTelemetry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry", @@ -29,9 +29,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", diff --git a/packages/opentelemetry/src/custom/hub.ts b/packages/opentelemetry/src/custom/hub.ts index ed37ae75b09f..2e0d120486dd 100644 --- a/packages/opentelemetry/src/custom/hub.ts +++ b/packages/opentelemetry/src/custom/hub.ts @@ -17,6 +17,7 @@ export class OpenTelemetryHub extends Hub { /** Custom getClient method that uses the custom hub. */ export function getClient(): C | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().getClient(); } @@ -95,7 +96,12 @@ export function getHubFromCarrier(carrier: Carrier): Hub { */ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub()): void { // If there's no hub on current domain, or it's an old API, assign a new one - if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { + if ( + !hasHubOnCarrier(carrier) || + // eslint-disable-next-line deprecation/deprecation + getHubFromCarrier(carrier).isOlderThan(API_VERSION) + ) { + // eslint-disable-next-line deprecation/deprecation const globalHubTopStack = parent.getStackTop(); setHubOnCarrier(carrier, new OpenTelemetryHub(globalHubTopStack.client, globalHubTopStack.scope.clone())); } @@ -103,7 +109,11 @@ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub( function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { // If there's no hub, or its an old API, assign a new one - if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { + if ( + !hasHubOnCarrier(registry) || + // eslint-disable-next-line deprecation/deprecation + getHubFromCarrier(registry).isOlderThan(API_VERSION) + ) { setHubOnCarrier(registry, new OpenTelemetryHub()); } diff --git a/packages/opentelemetry/src/custom/scope.ts b/packages/opentelemetry/src/custom/scope.ts index c6bdfb164900..2455d616ff39 100644 --- a/packages/opentelemetry/src/custom/scope.ts +++ b/packages/opentelemetry/src/custom/scope.ts @@ -46,6 +46,7 @@ export class OpenTelemetryScope extends Scope { newScope._attachments = [...this['_attachments']]; newScope._sdkProcessingMetadata = { ...this['_sdkProcessingMetadata'] }; newScope._propagationContext = { ...this['_propagationContext'] }; + newScope._client = this._client; return newScope; } diff --git a/packages/opentelemetry/src/custom/transaction.ts b/packages/opentelemetry/src/custom/transaction.ts index b1f84d01aec7..fe5001f8b050 100644 --- a/packages/opentelemetry/src/custom/transaction.ts +++ b/packages/opentelemetry/src/custom/transaction.ts @@ -8,9 +8,11 @@ import { uuid4 } from '@sentry/utils'; * with some OTEL specifics. */ export function startTransaction(hub: HubInterface, transactionContext: TransactionContext): Transaction { + // eslint-disable-next-line deprecation/deprecation const client = hub.getClient(); const options: Partial = (client && client.getOptions()) || {}; + // eslint-disable-next-line deprecation/deprecation const transaction = new OpenTelemetryTransaction(transactionContext, hub as Hub); // Since we do not do sampling here, we assume that this is _always_ sampled // Any sampling decision happens in OpenTelemetry's sampler @@ -36,6 +38,7 @@ export class OpenTelemetryTransaction extends Transaction { return undefined; } + // eslint-disable-next-line deprecation/deprecation const client = this._hub.getClient(); if (!client) { diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 8c515fc0afc9..ca2f1deb4fc0 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -3,8 +3,8 @@ import type { ExportResult } from '@opentelemetry/core'; import { ExportResultCode } from '@opentelemetry/core'; import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { flush } from '@sentry/core'; -import type { DynamicSamplingContext, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; +import { flush, getCurrentScope } from '@sentry/core'; +import type { DynamicSamplingContext, Scope, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; import { logger } from '@sentry/utils'; import { getCurrentHub } from './custom/hub'; @@ -110,7 +110,7 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { // Now finish the transaction, which will send it together with all the spans // We make sure to use the finish scope - const scope = getSpanFinishScope(span); + const scope = getScopeForTransactionFinish(span); transaction.finishWithScope(convertOtelTimeToSeconds(span.endTime), scope); }); @@ -119,6 +119,17 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { .filter((span): span is ReadableSpan => !!span); } +function getScopeForTransactionFinish(span: ReadableSpan): Scope { + // The finish scope should normally always be there (and it is already a clone), + // but for the sake of type safety we fall back to a clone of the current scope + const scope = getSpanFinishScope(span) || getCurrentScope().clone(); + scope.setContext('otel', { + attributes: removeSentryAttributes(span.attributes), + resource: span.resource.attributes, + }); + return scope; +} + function getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { return nodes.filter((node): node is SpanNodeCompleted => !!node.span && !node.parentNode); } @@ -176,11 +187,6 @@ function createTransactionForOtelSpan(span: ReadableSpan): OpenTelemetryTransact sampled: true, }) as OpenTelemetryTransaction; - transaction.setContext('otel', { - attributes: removeSentryAttributes(span.attributes), - resource: span.resource.attributes, - }); - return transaction; } @@ -204,6 +210,7 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, sentryParentSpan: Sentry const { op, description, tags, data, origin } = getSpanData(span); const allData = { ...removeSentryAttributes(attributes), ...data }; + // eslint-disable-next-line deprecation/deprecation const sentrySpan = sentryParentSpan.startChild({ description, op, @@ -273,9 +280,8 @@ function getTags(span: ReadableSpan): Record { const tags: Record = {}; if (attributes[SemanticAttributes.HTTP_STATUS_CODE]) { - const statusCode = attributes[SemanticAttributes.HTTP_STATUS_CODE] as string; - - tags['http.status_code'] = statusCode; + const statusCode = attributes[SemanticAttributes.HTTP_STATUS_CODE] as string | number; + tags['http.status_code'] = `${statusCode}`; } return tags; diff --git a/packages/opentelemetry/src/spanProcessor.ts b/packages/opentelemetry/src/spanProcessor.ts index fb05b2c645cb..ea2a2d0c0e75 100644 --- a/packages/opentelemetry/src/spanProcessor.ts +++ b/packages/opentelemetry/src/spanProcessor.ts @@ -31,8 +31,9 @@ function onSpanStart(span: Span, parentContext: Context, _ScopeClass: typeof Ope // We need the scope at time of span creation in order to apply it to the event when the span is finished if (actualHub) { + // eslint-disable-next-line deprecation/deprecation const scope = actualHub.getScope(); - setSpanScope(span, actualHub.getScope()); + setSpanScope(span, scope); setSpanHub(span, actualHub); // Use this scope for finishing the span diff --git a/packages/opentelemetry/src/types.ts b/packages/opentelemetry/src/types.ts index fdab000a6e09..168a9f4893a6 100644 --- a/packages/opentelemetry/src/types.ts +++ b/packages/opentelemetry/src/types.ts @@ -1,6 +1,6 @@ import type { Attributes, Span as WriteableSpan, SpanKind, TimeInput, Tracer } from '@opentelemetry/api'; import type { BasicTracerProvider, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'; -import type { SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; +import type { Scope, SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; export interface OpenTelemetryClient { tracer: Tracer; @@ -13,6 +13,7 @@ export interface OpenTelemetrySpanContext { metadata?: Partial; origin?: SpanOrigin; source?: TransactionSource; + scope?: Scope; // Base SpanOptions we support attributes?: Attributes; diff --git a/packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts b/packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts index 3dde27e49e9f..dcf02b671044 100644 --- a/packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts +++ b/packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts @@ -35,6 +35,7 @@ export function maybeCaptureExceptionForTimedEvent(hub: Hub, event: TimedEvent, syntheticError.name = type; } + // eslint-disable-next-line deprecation/deprecation hub.captureException(syntheticError, { captureContext: otelSpan ? { diff --git a/packages/opentelemetry/test/asyncContextStrategy.test.ts b/packages/opentelemetry/test/asyncContextStrategy.test.ts index 81c3adafa536..92856234bf80 100644 --- a/packages/opentelemetry/test/asyncContextStrategy.test.ts +++ b/packages/opentelemetry/test/asyncContextStrategy.test.ts @@ -29,12 +29,14 @@ describe('asyncContextStrategy', () => { test('hub scope inheritance', () => { const globalHub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation globalHub.setExtra('a', 'b'); runWithAsyncContext(() => { const hub1 = getCurrentHub(); expect(hub1).toEqual(globalHub); + // eslint-disable-next-line deprecation/deprecation hub1.setExtra('c', 'd'); expect(hub1).not.toEqual(globalHub); @@ -43,6 +45,7 @@ describe('asyncContextStrategy', () => { expect(hub2).toEqual(hub1); expect(hub2).not.toEqual(globalHub); + // eslint-disable-next-line deprecation/deprecation hub2.setExtra('e', 'f'); expect(hub2).not.toEqual(hub1); }); @@ -53,6 +56,7 @@ describe('asyncContextStrategy', () => { async function addRandomExtra(hub: Hub, key: string): Promise { return new Promise(resolve => { setTimeout(() => { + // eslint-disable-next-line deprecation/deprecation hub.setExtra(key, Math.random()); resolve(); }, 100); @@ -116,7 +120,9 @@ describe('asyncContextStrategy', () => { runWithAsyncContext(() => { const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation hub.getStack().push({ client: 'process' } as any); + // eslint-disable-next-line deprecation/deprecation expect(hub.getStack()[1]).toEqual({ client: 'process' }); // Just in case so we don't have to worry which one finishes first // (although it always should be d2) @@ -130,7 +136,9 @@ describe('asyncContextStrategy', () => { runWithAsyncContext(() => { const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation hub.getStack().push({ client: 'local' } as any); + // eslint-disable-next-line deprecation/deprecation expect(hub.getStack()[1]).toEqual({ client: 'local' }); setTimeout(() => { d2done = true; diff --git a/packages/opentelemetry/test/custom/hub.test.ts b/packages/opentelemetry/test/custom/hub.test.ts index e3f2eea70e6b..fac2405a4a90 100644 --- a/packages/opentelemetry/test/custom/hub.test.ts +++ b/packages/opentelemetry/test/custom/hub.test.ts @@ -10,6 +10,7 @@ describe('OpenTelemetryHub', () => { const hub2 = getCurrentHub(); expect(hub2).toBe(hub); + // eslint-disable-next-line deprecation/deprecation const scope = hub.getScope(); expect(scope).toBeDefined(); expect(scope).toBeInstanceOf(OpenTelemetryScope); @@ -18,6 +19,7 @@ describe('OpenTelemetryHub', () => { it('hub gets correct scope on initialization', () => { const hub = new OpenTelemetryHub(); + // eslint-disable-next-line deprecation/deprecation const scope = hub.getScope(); expect(scope).toBeDefined(); expect(scope).toBeInstanceOf(OpenTelemetryScope); @@ -30,6 +32,7 @@ describe('OpenTelemetryHub', () => { const scope = hub.pushScope(); expect(scope).toBeInstanceOf(OpenTelemetryScope); + // eslint-disable-next-line deprecation/deprecation const scope2 = hub.getScope(); expect(scope2).toBe(scope); }); @@ -37,6 +40,7 @@ describe('OpenTelemetryHub', () => { it('withScope() creates correct scope', () => { const hub = new OpenTelemetryHub(); + // eslint-disable-next-line deprecation/deprecation hub.withScope(scope => { expect(scope).toBeInstanceOf(OpenTelemetryScope); }); diff --git a/packages/opentelemetry/test/custom/scope.test.ts b/packages/opentelemetry/test/custom/scope.test.ts index 41827fcd772d..1662f571331c 100644 --- a/packages/opentelemetry/test/custom/scope.test.ts +++ b/packages/opentelemetry/test/custom/scope.test.ts @@ -81,12 +81,14 @@ describe('NodeExperimentalScope', () => { // Pretend we have a _span set scope['_span'] = {} as any; + // eslint-disable-next-line deprecation/deprecation expect(scope.getSpan()).toBeUndefined(); }); it('setSpan is a noop', () => { const scope = new OpenTelemetryScope(); + // eslint-disable-next-line deprecation/deprecation scope.setSpan({} as any); expect(scope['_span']).toBeUndefined(); diff --git a/packages/opentelemetry/test/custom/transaction.test.ts b/packages/opentelemetry/test/custom/transaction.test.ts index 043d76235140..5b1ccc5b8044 100644 --- a/packages/opentelemetry/test/custom/transaction.test.ts +++ b/packages/opentelemetry/test/custom/transaction.test.ts @@ -1,3 +1,4 @@ +import { spanToJSON } from '@sentry/core'; import { getCurrentHub } from '../../src/custom/hub'; import { OpenTelemetryScope } from '../../src/custom/scope'; import { OpenTelemetryTransaction, startTransaction } from '../../src/custom/transaction'; @@ -16,8 +17,8 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new OpenTelemetryTransaction({ name: 'test' }, hub); - transaction.sampled = true; + // eslint-disable-next-line deprecation/deprecation + const transaction = new OpenTelemetryTransaction({ name: 'test', sampled: true }, hub); const res = transaction.finishWithScope(); @@ -63,8 +64,8 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456 }, hub); - transaction.sampled = true; + // eslint-disable-next-line deprecation/deprecation + const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456, sampled: true }, hub); const res = transaction.finishWithScope(1234567); @@ -88,8 +89,8 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456 }, hub); - transaction.sampled = true; + // eslint-disable-next-line deprecation/deprecation + const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456, sampled: true }, hub); const scope = new OpenTelemetryScope(); scope.setTags({ @@ -148,16 +149,16 @@ describe('startTranscation', () => { const transaction = startTransaction(hub, { name: 'test' }); expect(transaction).toBeInstanceOf(OpenTelemetryTransaction); - - expect(transaction.sampled).toBe(undefined); + expect(transaction['_sampled']).toBe(undefined); expect(transaction.spanRecorder).toBeDefined(); expect(transaction.spanRecorder?.spans).toHaveLength(1); + // eslint-disable-next-line deprecation/deprecation expect(transaction.metadata).toEqual({ source: 'custom', spanMetadata: {}, }); - expect(transaction.toJSON()).toEqual( + expect(spanToJSON(transaction)).toEqual( expect.objectContaining({ origin: 'manual', span_id: expect.any(String), @@ -180,13 +181,13 @@ describe('startTranscation', () => { }); expect(transaction).toBeInstanceOf(OpenTelemetryTransaction); - + // eslint-disable-next-line deprecation/deprecation expect(transaction.metadata).toEqual({ source: 'custom', spanMetadata: {}, }); - expect(transaction.toJSON()).toEqual( + expect(spanToJSON(transaction)).toEqual( expect.objectContaining({ origin: 'manual', span_id: 'span1', diff --git a/packages/opentelemetry/test/helpers/initOtel.ts b/packages/opentelemetry/test/helpers/initOtel.ts index 052812c12d33..ba42f221b3d8 100644 --- a/packages/opentelemetry/test/helpers/initOtel.ts +++ b/packages/opentelemetry/test/helpers/initOtel.ts @@ -7,7 +7,7 @@ import { SDK_VERSION } from '@sentry/core'; import { logger } from '@sentry/utils'; import { wrapContextManagerClass } from '../../src/contextManager'; -import { getCurrentHub } from '../../src/custom/hub'; +import { getClient } from '../../src/custom/hub'; import { DEBUG_BUILD } from '../../src/debug-build'; import { SentryPropagator } from '../../src/propagator'; import { SentrySampler } from '../../src/sampler'; @@ -19,7 +19,7 @@ import type { TestClientInterface } from './TestClient'; * Initialize OpenTelemetry for Node. */ export function initOtel(): void { - const client = getCurrentHub().getClient(); + const client = getClient(); if (!client) { DEBUG_BUILD && diff --git a/packages/opentelemetry/test/integration/breadcrumbs.test.ts b/packages/opentelemetry/test/integration/breadcrumbs.test.ts index af095be83f76..bfb40a348ff9 100644 --- a/packages/opentelemetry/test/integration/breadcrumbs.test.ts +++ b/packages/opentelemetry/test/integration/breadcrumbs.test.ts @@ -1,6 +1,6 @@ -import { withScope } from '@sentry/core'; +import { addBreadcrumb, captureException, withScope } from '@sentry/core'; -import { OpenTelemetryHub, getCurrentHub } from '../../src/custom/hub'; +import { OpenTelemetryHub, getClient, getCurrentHub } from '../../src/custom/hub'; import { startSpan } from '../../src/trace'; import type { TestClientInterface } from '../helpers/TestClient'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; @@ -20,16 +20,17 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb }); const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; expect(hub).toBeInstanceOf(OpenTelemetryHub); - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); - hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); - hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + addBreadcrumb({ timestamp: 123455, message: 'test3' }); const error = new Error('test'); - hub.captureException(error); + // eslint-disable-next-line deprecation/deprecation + captureException(error); await client.flush(); @@ -59,25 +60,26 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb }); const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; expect(hub).toBeInstanceOf(OpenTelemetryHub); const error = new Error('test'); - hub.addBreadcrumb({ timestamp: 123456, message: 'test0' }); + addBreadcrumb({ timestamp: 123456, message: 'test0' }); withScope(() => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); }); withScope(() => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test2' }); - hub.captureException(error); + addBreadcrumb({ timestamp: 123456, message: 'test2' }); + // eslint-disable-next-line deprecation/deprecation + captureException(error); }); withScope(() => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test3' }); + addBreadcrumb({ timestamp: 123456, message: 'test3' }); }); await client.flush(); @@ -107,23 +109,22 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; const error = new Error('test'); startSpan({ name: 'test' }, () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); startSpan({ name: 'inner1' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); }); startSpan({ name: 'inner2' }, () => { - hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + addBreadcrumb({ timestamp: 123455, message: 'test3' }); }); - hub.captureException(error); + captureException(error); }); await client.flush(); @@ -153,27 +154,26 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; const error = new Error('test'); startSpan({ name: 'test1' }, () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1-a' }); + addBreadcrumb({ timestamp: 123456, message: 'test1-a' }); startSpan({ name: 'inner1' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test1-b' }); + addBreadcrumb({ timestamp: 123457, message: 'test1-b' }); }); }); startSpan({ name: 'test2' }, () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test2-a' }); + addBreadcrumb({ timestamp: 123456, message: 'test2-a' }); startSpan({ name: 'inner2' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); }); - hub.captureException(error); + captureException(error); }); await client.flush(); @@ -202,20 +202,19 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; const error = new Error('test'); startSpan({ name: 'test1' }, () => { withScope(() => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); }); startSpan({ name: 'inner1' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + addBreadcrumb({ timestamp: 123457, message: 'test2' }); }); - hub.captureException(error); + captureException(error); }); await client.flush(); @@ -244,36 +243,35 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; const error = new Error('test'); startSpan({ name: 'test1' }, () => { withScope(() => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); }); startSpan({ name: 'inner1' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + addBreadcrumb({ timestamp: 123457, message: 'test2' }); startSpan({ name: 'inner2' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test3' }); + addBreadcrumb({ timestamp: 123457, message: 'test3' }); startSpan({ name: 'inner3' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test4' }); + addBreadcrumb({ timestamp: 123457, message: 'test4' }); - hub.captureException(error); + captureException(error); startSpan({ name: 'inner4' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test5' }); + addBreadcrumb({ timestamp: 123457, message: 'test5' }); }); - hub.addBreadcrumb({ timestamp: 123457, message: 'test6' }); + addBreadcrumb({ timestamp: 123457, message: 'test6' }); }); }); }); - hub.addBreadcrumb({ timestamp: 123456, message: 'test99' }); + addBreadcrumb({ timestamp: 123456, message: 'test99' }); }); await client.flush(); @@ -303,36 +301,35 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; const error = new Error('test'); const promise1 = startSpan({ name: 'test' }, async () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123456, message: 'test1' }); await startSpan({ name: 'inner1' }, async () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + addBreadcrumb({ timestamp: 123457, message: 'test2' }); }); await startSpan({ name: 'inner2' }, async () => { - hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + addBreadcrumb({ timestamp: 123455, message: 'test3' }); }); await new Promise(resolve => setTimeout(resolve, 10)); - hub.captureException(error); + captureException(error); }); const promise2 = startSpan({ name: 'test-b' }, async () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); + addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); await startSpan({ name: 'inner1' }, async () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); }); await startSpan({ name: 'inner2' }, async () => { - hub.addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); + addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); }); }); diff --git a/packages/opentelemetry/test/integration/otelTimedEvents.test.ts b/packages/opentelemetry/test/integration/otelTimedEvents.test.ts index ec25108821f5..8611f366d04a 100644 --- a/packages/opentelemetry/test/integration/otelTimedEvents.test.ts +++ b/packages/opentelemetry/test/integration/otelTimedEvents.test.ts @@ -1,6 +1,6 @@ import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { getCurrentHub } from '../../src/custom/hub'; +import { getClient } from '../../src/custom/hub'; import { startSpan } from '../../src/trace'; import type { TestClientInterface } from '../helpers/TestClient'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; @@ -16,8 +16,7 @@ describe('Integration | OTEL TimedEvents', () => { mockSdkInit({ beforeSend, beforeSendTransaction, enableTracing: true }); - const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; startSpan({ name: 'test' }, span => { span.addEvent('exception', { diff --git a/packages/opentelemetry/test/integration/scope.test.ts b/packages/opentelemetry/test/integration/scope.test.ts index 094f926bd30e..f313dcba352e 100644 --- a/packages/opentelemetry/test/integration/scope.test.ts +++ b/packages/opentelemetry/test/integration/scope.test.ts @@ -1,6 +1,6 @@ -import { captureException, setTag, withScope } from '@sentry/core'; +import { captureException, getCurrentScope, setTag, withScope } from '@sentry/core'; -import { OpenTelemetryHub, getCurrentHub } from '../../src/custom/hub'; +import { OpenTelemetryHub, getClient, getCurrentHub } from '../../src/custom/hub'; import { OpenTelemetryScope } from '../../src/custom/scope'; import { startSpan } from '../../src/trace'; import { getSpanScope } from '../../src/utils/spanData'; @@ -23,9 +23,9 @@ describe('Integration | Scope', () => { mockSdkInit({ enableTracing, beforeSend, beforeSendTransaction }); const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; - const rootScope = hub.getScope(); + const rootScope = getCurrentScope(); expect(hub).toBeInstanceOf(OpenTelemetryHub); expect(rootScope).toBeInstanceOf(OpenTelemetryScope); @@ -128,9 +128,8 @@ describe('Integration | Scope', () => { mockSdkInit({ enableTracing, beforeSend, beforeSendTransaction }); const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; - - const rootScope = hub.getScope(); + const client = getClient() as TestClientInterface; + const rootScope = getCurrentScope(); expect(hub).toBeInstanceOf(OpenTelemetryHub); expect(rootScope).toBeInstanceOf(OpenTelemetryScope); diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 787d251b0558..78a9aa18fde7 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -4,7 +4,8 @@ import { addBreadcrumb, setTag } from '@sentry/core'; import type { PropagationContext, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; -import { getCurrentHub } from '../../src/custom/hub'; +import { spanToJSON } from '@sentry/core'; +import { getClient } from '../../src/custom/hub'; import { SentrySpanProcessor } from '../../src/spanProcessor'; import { startInactiveSpan, startSpan } from '../../src/trace'; import { setPropagationContextOnContext } from '../../src/utils/contextData'; @@ -22,8 +23,7 @@ describe('Integration | Transactions', () => { mockSdkInit({ enableTracing: true, beforeSendTransaction }); - const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); setTag('outer.tag', 'test value'); @@ -142,7 +142,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', @@ -173,8 +173,7 @@ describe('Integration | Transactions', () => { mockSdkInit({ enableTracing: true, beforeSendTransaction }); - const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); @@ -331,8 +330,7 @@ describe('Integration | Transactions', () => { mockSdkInit({ enableTracing: true, beforeSendTransaction }); - const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; // We simulate the correct context we'd normally get from the SentryPropagator context.with( @@ -393,7 +391,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', @@ -431,8 +429,7 @@ describe('Integration | Transactions', () => { mockSdkInit({ enableTracing: true, beforeSendTransaction }); - const hub = getCurrentHub(); - const client = hub.getClient() as TestClientInterface; + const client = getClient() as TestClientInterface; const provider = getProvider(); const multiSpanProcessor = provider?.activeSpanProcessor as | (SpanProcessor & { _spanProcessors?: SpanProcessor[] }) diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 54a1082830ae..af8625a6534e 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -4,7 +4,7 @@ import { TraceFlags, context, trace } from '@opentelemetry/api'; import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import type { PropagationContext } from '@sentry/types'; -import { getCurrentHub } from '../src/custom/hub'; +import { getClient } from '../src/custom/hub'; import { InternalSentrySemanticAttributes } from '../src/semanticAttributes'; import { startInactiveSpan, startSpan, startSpanManual } from '../src/trace'; import type { AbstractSpan } from '../src/types'; @@ -503,7 +503,7 @@ describe('trace (sampling)', () => { // Now let's mutate the tracesSampleRate so that the next entry _should_ not be sampled // but it will because of parent sampling - const client = getCurrentHub().getClient(); + const client = getClient(); client!.getOptions().tracesSampleRate = 0.5; startSpan({ name: 'inner' }, innerSpan => { @@ -526,7 +526,7 @@ describe('trace (sampling)', () => { // Now let's mutate the tracesSampleRate so that the next entry _should_ be sampled // but it will remain unsampled because of parent sampling - const client = getCurrentHub().getClient(); + const client = getClient(); client!.getOptions().tracesSampleRate = 1; startSpan({ name: 'inner' }, innerSpan => { diff --git a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts index 887a7f6bc803..a73067581b9a 100644 --- a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts +++ b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts @@ -45,6 +45,7 @@ describe('setupEventContextTrace', () => { it('works with no active span', async () => { const error = new Error('test'); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); await client.flush(); @@ -79,6 +80,7 @@ describe('setupEventContextTrace', () => { client.tracer.startActiveSpan('inner', innerSpan => { innerId = innerSpan?.spanContext().spanId; + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); }); diff --git a/packages/react/package.json b/packages/react/package.json index 437642015f78..ef96cbed789b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -29,9 +29,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry/browser": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 749da5e23167..4a233407eab1 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -59,6 +59,7 @@ class Profiler extends React.Component { const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation this._mountSpan = activeTransaction.startChild({ description: `<${name}>`, op: REACT_MOUNT_OP, @@ -85,6 +86,7 @@ class Profiler extends React.Component { const changedProps = Object.keys(updateProps).filter(k => updateProps[k] !== this.props.updateProps[k]); if (changedProps.length > 0) { const now = timestampInSeconds(); + // eslint-disable-next-line deprecation/deprecation this._updateSpan = this._mountSpan.startChild({ data: { changedProps, @@ -116,6 +118,7 @@ class Profiler extends React.Component { if (this._mountSpan && includeRender) { // If we were able to obtain the spanId of the mount activity, we should set the // next activity as a child to the component mount activity. + // eslint-disable-next-line deprecation/deprecation this._mountSpan.startChild({ description: `<${name}>`, endTimestamp: timestampInSeconds(), @@ -183,6 +186,7 @@ function useProfiler( const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation return activeTransaction.startChild({ description: `<${name}>`, op: REACT_MOUNT_OP, @@ -201,6 +205,7 @@ function useProfiler( return (): void => { if (mountSpan && options.hasRenderSpan) { + // eslint-disable-next-line deprecation/deprecation mountSpan.startChild({ description: `<${name}>`, endTimestamp: timestampInSeconds(), @@ -221,7 +226,9 @@ export { withProfiler, Profiler, useProfiler }; /** Grabs active transaction off scope */ export function getActiveTransaction(hub: Hub = getCurrentHub()): T | undefined { if (hub) { + // eslint-disable-next-line deprecation/deprecation const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation return scope.getTransaction() as T | undefined; } diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 8a42c5ff96f1..04995ee4bc44 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -1,4 +1,5 @@ import { WINDOW } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { Transaction, TransactionSource } from '@sentry/types'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; @@ -166,7 +167,7 @@ export function withSentryRouting

, R extends React const WrappedRoute: React.FC

= (props: P) => { if (activeTransaction && props && props.computedMatch && props.computedMatch.isExact) { activeTransaction.updateName(props.computedMatch.path); - activeTransaction.setMetadata({ source: 'route' }); + activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } // @ts-expect-error Setting more specific React Component typing for `R` generic above diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 920a6b4f8a0d..de87e5bb6881 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -2,6 +2,7 @@ // https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536 import { WINDOW } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getNumberOfUrlSegments, logger } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; @@ -136,7 +137,7 @@ function updatePageloadTransaction( if (activeTransaction && branches) { const [name, source] = getNormalizedName(routes, location, branches, basename); activeTransaction.updateName(name); - activeTransaction.setMetadata({ source }); + activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } } diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 2b06b3a196a5..5849bb688598 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX @@ -12,7 +13,7 @@ describe('React Router v4', () => { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; routes?: RouteConfig[]; - }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts && _opts.routes !== undefined ? matchPath : undefined, routes: undefined, @@ -23,16 +24,16 @@ describe('React Router v4', () => { const history = createMemoryHistory(); const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV4Instrumentation(history, options.routes, options.matchPath)( mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange, ); - return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetAttribute }]; } it('starts a pageload transaction when instrumentation is started', () => { @@ -169,7 +170,7 @@ describe('React Router v4', () => { }); it('normalizes transaction name with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -196,11 +197,11 @@ describe('React Router v4', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/users/:userid'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('normalizes nested transaction names with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -227,7 +228,7 @@ describe('React Router v4', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid/v1/:teamid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); act(() => { history.push('/organizations/543'); @@ -244,7 +245,7 @@ describe('React Router v4', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(3); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('matches with route object', () => { diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index fba57df9a5f8..c571b3590b8f 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX @@ -12,7 +13,7 @@ describe('React Router v5', () => { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; routes?: RouteConfig[]; - }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts && _opts.routes !== undefined ? matchPath : undefined, routes: undefined, @@ -23,16 +24,16 @@ describe('React Router v5', () => { const history = createMemoryHistory(); const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV5Instrumentation(history, options.routes, options.matchPath)( mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange, ); - return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetAttribute }]; } it('starts a pageload transaction when instrumentation is started', () => { @@ -169,7 +170,7 @@ describe('React Router v5', () => { }); it('normalizes transaction name with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -196,11 +197,11 @@ describe('React Router v5', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/users/:userid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('normalizes nested transaction names with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -228,7 +229,7 @@ describe('React Router v5', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid/v1/:teamid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); act(() => { history.push('/organizations/543'); diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx index a89bb50e1f82..d6b9c0c45b49 100644 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ b/packages/react/test/reactrouterv6.4.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { render } from '@testing-library/react'; import { Request } from 'node-fetch'; import * as React from 'react'; @@ -25,7 +26,7 @@ describe('React Router v6.4', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; - }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts ? matchPath : undefined, startTransactionOnLocationChange: true, @@ -34,10 +35,10 @@ describe('React Router v6.4', () => { }; const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV6Instrumentation( React.useEffect, @@ -46,7 +47,7 @@ describe('React Router v6.4', () => { createRoutesFromChildren, matchRoutes, )(mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange); - return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetAttribute }]; } describe('wrapCreateBrowserRouter', () => { @@ -246,7 +247,7 @@ describe('React Router v6.4', () => { }); it('updates pageload transaction to a parameterized route', () => { - const [mockStartTransaction, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); const router = sentryCreateBrowserRouter( @@ -272,7 +273,7 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockUpdateName).toHaveBeenLastCalledWith('/about/:page'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('works with `basename` option', () => { diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index 965ce134bb74..df30c4596dbf 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { render } from '@testing-library/react'; import * as React from 'react'; import { @@ -21,7 +22,7 @@ describe('React Router v6', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; - }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts ? matchPath : undefined, startTransactionOnLocationChange: true, @@ -30,10 +31,10 @@ describe('React Router v6', () => { }; const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV6Instrumentation( React.useEffect, @@ -42,7 +43,7 @@ describe('React Router v6', () => { createRoutesFromChildren, matchRoutes, )(mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange); - return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetAttribute }]; } describe('withSentryReactRouterV6Routing', () => { @@ -545,7 +546,7 @@ describe('React Router v6', () => { }); it('does not add double slashes to URLS', () => { - const [mockStartTransaction, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const wrappedUseRoutes = wrapUseRoutes(useRoutes); const Routes = () => @@ -588,11 +589,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); // should be /tests not //tests expect(mockUpdateName).toHaveBeenLastCalledWith('/tests'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('handles wildcard routes properly', () => { - const [mockStartTransaction, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const wrappedUseRoutes = wrapUseRoutes(useRoutes); const Routes = () => @@ -634,7 +635,7 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockUpdateName).toHaveBeenLastCalledWith('/tests/:testId/*'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); }); }); diff --git a/packages/remix/package.json b/packages/remix/package.json index 96ce798d97db..41f335a5b64f 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -35,11 +35,11 @@ }, "dependencies": { "@sentry/cli": "^2.23.0", - "@sentry/core": "7.92.0", - "@sentry/node": "7.92.0", - "@sentry/react": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry/core": "7.93.0", + "@sentry/node": "7.93.0", + "@sentry/react": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "glob": "^10.3.4", "yargs": "^17.6.0" }, diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index 597f6daae48f..fc395e8ddedc 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { ErrorBoundaryProps } from '@sentry/react'; import { WINDOW, withErrorBoundary } from '@sentry/react'; import type { Transaction, TransactionContext } from '@sentry/types'; @@ -126,7 +127,7 @@ export function withSentry

, R extends React.Co _useEffect(() => { if (activeTransaction && matches && matches.length) { activeTransaction.updateName(matches[matches.length - 1].id); - activeTransaction.setMetadata({ source: 'route' }); + activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } isBaseLocation = true; diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index bba1a71f1e3f..c73b5c84962a 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -25,6 +25,7 @@ export { createTransport, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -44,6 +45,7 @@ export { // eslint-disable-next-line deprecation/deprecation trace, withScope, + withIsolationScope, autoDiscoverNodePerformanceMonitoringIntegrations, makeNodeTransport, defaultIntegrations, diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index e07710dc340a..7a7ec540e469 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -1,10 +1,13 @@ /* eslint-disable max-lines */ import { + getActiveSpan, getActiveTransaction, getClient, getCurrentScope, + getDynamicSamplingContextFromSpan, hasTracingEnabled, runWithAsyncContext, + spanToJSON, spanToTraceHeader, } from '@sentry/core'; import type { Hub } from '@sentry/node'; @@ -140,7 +143,9 @@ export async function captureRemixServerException(err: unknown, name: string, re const objectifiedErr = objectify(err); captureException(isResponse(objectifiedErr) ? await extractResponseError(objectifiedErr) : objectifiedErr, scope => { - const activeTransactionName = getActiveTransaction()?.name; + // eslint-disable-next-line deprecation/deprecation + const transaction = getActiveTransaction(); + const activeTransactionName = transaction ? spanToJSON(transaction) : undefined; scope.setSDKProcessingMetadata({ request: { @@ -181,13 +186,15 @@ function makeWrappedDocumentRequestFunction(remixVersion?: number) { loadContext?: Record, ): Promise { let res: Response; + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); try { + // eslint-disable-next-line deprecation/deprecation const span = activeTransaction?.startChild({ op: 'function.remix.document_request', origin: 'auto.function.remix', - description: activeTransaction.name, + description: spanToJSON(activeTransaction).description, tags: { method: request.method, url: request.url, @@ -235,10 +242,12 @@ function makeWrappedDataFunction( } let res: Response | AppData; + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); const currentScope = getCurrentScope(); try { + // eslint-disable-next-line deprecation/deprecation const span = activeTransaction?.startChild({ op: `function.remix.${name}`, origin: 'auto.ui.remix', @@ -250,11 +259,13 @@ function makeWrappedDataFunction( if (span) { // Assign data function to hub to be able to see `db` transactions (if any) as children. + // eslint-disable-next-line deprecation/deprecation currentScope.setSpan(span); } res = await origFn.call(this, args); + // eslint-disable-next-line deprecation/deprecation currentScope.setSpan(activeTransaction); span?.end(); } catch (err) { @@ -290,14 +301,14 @@ function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string; } { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); - const currentScope = getCurrentScope(); if (isNodeEnv() && hasTracingEnabled()) { - const span = currentScope.getSpan(); + const span = getActiveSpan(); if (span && transaction) { - const dynamicSamplingContext = transaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction); return { sentryTrace: spanToTraceHeader(span), @@ -392,6 +403,7 @@ export function startRequestHandlerTransaction( request.headers['sentry-trace'], request.headers.baggage, ); + // eslint-disable-next-line deprecation/deprecation hub.getScope().setPropagationContext(propagationContext); // TODO: Refactor this to `startSpan()` @@ -410,6 +422,7 @@ export function startRequestHandlerTransaction( }, }); + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(transaction); return transaction; } diff --git a/packages/remix/test/index.client.test.ts b/packages/remix/test/index.client.test.ts index d835d88b3035..af4861968bf7 100644 --- a/packages/remix/test/index.client.test.ts +++ b/packages/remix/test/index.client.test.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import * as SentryReact from '@sentry/react'; import { GLOBAL_OBJ } from '@sentry/utils'; @@ -39,7 +39,7 @@ describe('Client init()', () => { }); it('sets runtime on scope', () => { - const currentScope = getCurrentHub().getScope(); + const currentScope = getCurrentScope(); // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); diff --git a/packages/remix/test/index.server.test.ts b/packages/remix/test/index.server.test.ts index 8568dc3c0cdb..46e1bd409afb 100644 --- a/packages/remix/test/index.server.test.ts +++ b/packages/remix/test/index.server.test.ts @@ -1,5 +1,4 @@ import * as SentryNode from '@sentry/node'; -import { getCurrentHub } from '@sentry/node'; import { GLOBAL_OBJ } from '@sentry/utils'; import { Integrations, init } from '../src/index.server'; @@ -47,7 +46,7 @@ describe('Server init()', () => { }); it('sets runtime on scope', () => { - const currentScope = getCurrentHub().getScope(); + const currentScope = SentryNode.getCurrentScope(); // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index d7391a59fd81..e376518dfe3c 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-worker", - "version": "7.92.0", + "version": "7.93.0", "description": "Worker for @sentry/replay", "main": "build/npm/esm/index.js", "module": "build/npm/esm/index.js", diff --git a/packages/replay/jest.setup.ts b/packages/replay/jest.setup.ts index 09e6c68a4483..eaba8ee05179 100644 --- a/packages/replay/jest.setup.ts +++ b/packages/replay/jest.setup.ts @@ -1,6 +1,6 @@ import { TextEncoder } from 'util'; /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { getCurrentHub } from '@sentry/core'; +import { getClient } from '@sentry/core'; import type { ReplayRecordingData, Transport } from '@sentry/types'; import * as SentryUtils from '@sentry/utils'; @@ -155,7 +155,7 @@ const toHaveSentReplay = function ( _received: jest.Mocked, expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean }, ) { - const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock; + const { calls } = (getClient()?.getTransport()?.send as MockTransport).mock; let result: CheckCallForSentReplayResult; @@ -214,7 +214,7 @@ const toHaveLastSentReplay = function ( _received: jest.Mocked, expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean }, ) { - const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock; + const { calls } = (getClient()?.getTransport()?.send as MockTransport).mock; const replayCalls = getReplayCalls(calls); const lastCall = replayCalls[calls.length - 1]?.[0]; diff --git a/packages/replay/package.json b/packages/replay/package.json index 54e8f7f11988..02a23ffc1df1 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/replay", - "version": "7.92.0", + "version": "7.93.0", "description": "User replays for Sentry", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -53,17 +53,17 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/replay-worker": "7.92.0", - "@sentry-internal/rrweb": "2.6.0", - "@sentry-internal/rrweb-snapshot": "2.6.0", + "@sentry-internal/replay-worker": "7.93.0", + "@sentry-internal/rrweb": "2.7.3", + "@sentry-internal/rrweb-snapshot": "2.7.3", "fflate": "^0.8.1", "jsdom-worker": "^0.2.1" }, "dependencies": { - "@sentry-internal/tracing": "7.92.0", - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry-internal/tracing": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "engines": { "node": ">=12" diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 75ec17f3627e..27513fe5643d 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -1,6 +1,12 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up import { EventType, record } from '@sentry-internal/rrweb'; -import { captureException, getClient, getCurrentScope } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + captureException, + getClient, + getCurrentScope, + spanToJSON, +} from '@sentry/core'; import type { ReplayRecordingMode, Transaction } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -699,12 +705,16 @@ export class ReplayContainer implements ReplayContainerInterface { * This is only available if performance is enabled, and if an instrumented router is used. */ public getCurrentRoute(): string | undefined { + // eslint-disable-next-line deprecation/deprecation const lastTransaction = this.lastTransaction || getCurrentScope().getTransaction(); - if (!lastTransaction || !['route', 'custom'].includes(lastTransaction.metadata.source)) { + + const attributes = (lastTransaction && spanToJSON(lastTransaction).data) || {}; + const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + if (!lastTransaction || !source || !['route', 'custom'].includes(source)) { return undefined; } - return lastTransaction.name; + return spanToJSON(lastTransaction).description; } /** diff --git a/packages/replay/test/integration/beforeAddRecordingEvent.test.ts b/packages/replay/test/integration/beforeAddRecordingEvent.test.ts index cfa0e80e17f6..68467f7bcafc 100644 --- a/packages/replay/test/integration/beforeAddRecordingEvent.test.ts +++ b/packages/replay/test/integration/beforeAddRecordingEvent.test.ts @@ -72,7 +72,7 @@ describe('Integration | beforeAddRecordingEvent', () => { mockSendReplayRequest = jest.spyOn(SendReplayRequest, 'sendReplayRequest'); jest.runAllTimers(); - mockTransportSend = SentryCore.getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend; + mockTransportSend = SentryCore.getClient()?.getTransport()?.send as MockTransportSend; }); beforeEach(() => { diff --git a/packages/replay/test/integration/coreHandlers/handleScope.test.ts b/packages/replay/test/integration/coreHandlers/handleScope.test.ts index f77f165981ef..0a704e626d3e 100644 --- a/packages/replay/test/integration/coreHandlers/handleScope.test.ts +++ b/packages/replay/test/integration/coreHandlers/handleScope.test.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import * as HandleScope from '../../../src/coreHandlers/handleScope'; import { mockSdk } from './../../index'; @@ -22,7 +22,7 @@ describe('Integration | coreHandlers | handleScope', () => { expect(mockHandleScopeListener).toHaveBeenCalledTimes(1); - getCurrentHub().getScope().addBreadcrumb({ category: 'console', message: 'testing' }); + getCurrentScope().addBreadcrumb({ category: 'console', message: 'testing' }); expect(mockHandleScope).toHaveBeenCalledTimes(1); expect(mockHandleScope).toHaveReturnedWith(expect.objectContaining({ category: 'console', message: 'testing' })); @@ -31,7 +31,7 @@ describe('Integration | coreHandlers | handleScope', () => { // This will trigger breadcrumb/scope listener, but handleScope should return // null because breadcrumbs has not changed - getCurrentHub().getScope().setUser({ email: 'foo@foo.com' }); + getCurrentScope().setUser({ email: 'foo@foo.com' }); expect(mockHandleScope).toHaveBeenCalledTimes(1); expect(mockHandleScope).toHaveReturnedWith(null); }); diff --git a/packages/replay/test/integration/rateLimiting.test.ts b/packages/replay/test/integration/rateLimiting.test.ts index 2a45598d0b7b..70cba8f35eff 100644 --- a/packages/replay/test/integration/rateLimiting.test.ts +++ b/packages/replay/test/integration/rateLimiting.test.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import { getClient } from '@sentry/core'; import type { Transport, TransportMakeRequestResponse } from '@sentry/types'; import { DEFAULT_FLUSH_MIN_DELAY } from '../../src/constants'; @@ -30,7 +30,7 @@ describe('Integration | rate-limiting behaviour', () => { }, })); - mockTransportSend = getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend; + mockTransportSend = getClient()?.getTransport()?.send as MockTransportSend; }); afterEach(async () => { diff --git a/packages/replay/test/integration/sendReplayEvent.test.ts b/packages/replay/test/integration/sendReplayEvent.test.ts index 8ca71236bb41..ff887c4b629b 100644 --- a/packages/replay/test/integration/sendReplayEvent.test.ts +++ b/packages/replay/test/integration/sendReplayEvent.test.ts @@ -48,7 +48,7 @@ describe('Integration | sendReplayEvent', () => { mockSendReplayRequest = jest.spyOn(SendReplayRequest, 'sendReplayRequest'); jest.runAllTimers(); - mockTransportSend = SentryCore.getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend; + mockTransportSend = SentryCore.getClient()?.getTransport()?.send as MockTransportSend; }); beforeEach(() => { diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index 86e1fa16cc9e..0485fa78dd95 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import { getClient } from '@sentry/core'; import type { Transport } from '@sentry/types'; import { @@ -43,7 +43,7 @@ describe('Integration | session', () => { }, })); - const mockTransport = getCurrentHub()?.getClient()?.getTransport()?.send as jest.MockedFunction; + const mockTransport = getClient()?.getTransport()?.send as jest.MockedFunction; mockTransport?.mockClear(); }); diff --git a/packages/replay/test/unit/util/prepareReplayEvent.test.ts b/packages/replay/test/unit/util/prepareReplayEvent.test.ts index 867bb801907a..06b464425808 100644 --- a/packages/replay/test/unit/util/prepareReplayEvent.test.ts +++ b/packages/replay/test/unit/util/prepareReplayEvent.test.ts @@ -1,24 +1,17 @@ -import type { Hub, Scope } from '@sentry/core'; +import { getClient, getCurrentScope } from '@sentry/core'; import { getCurrentHub } from '@sentry/core'; -import type { Client, ReplayEvent } from '@sentry/types'; +import type { ReplayEvent } from '@sentry/types'; import { REPLAY_EVENT_NAME } from '../../../src/constants'; import { prepareReplayEvent } from '../../../src/util/prepareReplayEvent'; import { TestClient, getDefaultClientOptions } from '../../utils/TestClient'; describe('Unit | util | prepareReplayEvent', () => { - let hub: Hub; - let client: Client; - let scope: Scope; - beforeEach(() => { - hub = getCurrentHub(); - client = new TestClient(getDefaultClientOptions()); + const hub = getCurrentHub(); + const client = new TestClient(getDefaultClientOptions()); hub.bindClient(client); - client = hub.getClient()!; - scope = hub.getScope(); - jest.spyOn(client, 'getSdkMetadata').mockImplementation(() => { return { sdk: { @@ -34,6 +27,9 @@ describe('Unit | util | prepareReplayEvent', () => { }); it('works', async () => { + const client = getClient()!; + const scope = getCurrentScope()!; + expect(client).toBeDefined(); expect(scope).toBeDefined(); diff --git a/packages/serverless/package.json b/packages/serverless/package.json index 3e7794af99f0..e771ac2ac78e 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/serverless", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for various serverless solutions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", @@ -29,10 +29,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.92.0", - "@sentry/node": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry/core": "7.93.0", + "@sentry/node": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "@types/aws-lambda": "^8.10.62", "@types/express": "^4.17.14" }, diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index d9cbe177efc8..9cf05355ee93 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -22,6 +22,7 @@ import { isString, logger } from '@sentry/utils'; import type { Context, Handler } from 'aws-lambda'; import { performance } from 'perf_hooks'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { AWSServices } from './awsservices'; import { DEBUG_BUILD } from './debug-build'; import { markEventUnhandled } from './utils'; @@ -223,7 +224,10 @@ function enhanceScopeWithEnvironmentData(scope: Scope, context: Context, startTi * @param context AWS Lambda context that will be used to extract some part of the data */ function enhanceScopeWithTransactionData(scope: Scope, context: Context): void { - scope.setTransactionName(context.functionName); + scope.addEventProcessor(event => { + event.transaction = context.functionName; + return event; + }); scope.setTag('server_name', process.env._AWS_XRAY_DAEMON_ADDRESS || process.env.SENTRY_NAME || hostname()); scope.setTag('url', `awslambda:///${context.functionName}`); } @@ -348,9 +352,8 @@ export function wrapHandler( op: 'function.aws.lambda', origin: 'auto.function.serverless', ...continueTraceContext, - metadata: { - ...continueTraceContext.metadata, - source: 'component', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', }, }, span => { diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index e7404b0695c4..41b73f58464e 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -58,10 +58,12 @@ function wrapMakeRequest( return function (this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback) { let span: Span | undefined; const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); const req = orig.call(this, operation, params); req.on('afterBuild', () => { if (transaction) { + // eslint-disable-next-line deprecation/deprecation span = transaction.startChild({ description: describe(this, operation, params), op: 'http.client', diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index cde99b9707b5..92a3eb0e37e7 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -1,4 +1,4 @@ -import { handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core'; import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node'; import { logger } from '@sentry/utils'; @@ -36,7 +36,7 @@ function _wrapCloudEventFunction( name: context.type || '', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, }, span => { const scope = getCurrentScope(); diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index 539c0ee80094..79c609e9108c 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -1,4 +1,4 @@ -import { handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core'; import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node'; import { logger } from '@sentry/utils'; @@ -39,7 +39,7 @@ function _wrapEventFunction name: context.eventType, op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, }, span => { const scope = getCurrentScope(); diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 609a75c81c1e..41fa620779c7 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -1,4 +1,4 @@ -import { Transaction, handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction, handleCallbackErrors } from '@sentry/core'; import type { AddRequestDataToEventOptions } from '@sentry/node'; import { continueTrace, startSpanManual } from '@sentry/node'; import { getCurrentScope } from '@sentry/node'; @@ -79,10 +79,8 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial { diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index 9f2ea37203b4..74a88f622333 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -109,8 +109,10 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str } let span: Span | undefined; const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); if (transaction) { + // eslint-disable-next-line deprecation/deprecation span = transaction.startChild({ description: `${callType} ${methodName}`, op: `grpc.${serviceIdentifier}`, diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index 6b522facf5ae..87687a52f82e 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -53,9 +53,11 @@ function wrapRequestFunction(orig: RequestFunction): RequestFunction { return function (this: common.Service, reqOpts: RequestOptions, callback: ResponseCallback): void { let span: Span | undefined; const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); if (transaction) { const httpMethod = reqOpts.method || 'GET'; + // eslint-disable-next-line deprecation/deprecation span = transaction.startChild({ description: `${httpMethod} ${reqOpts.uri}`, op: `http.client.${identifyService(this.apiEndpoint)}`, diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index e3daf04b85da..4662f0f48d35 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -27,6 +27,7 @@ export { // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getCurrentHub, getClient, @@ -44,6 +45,7 @@ export { // eslint-disable-next-line deprecation/deprecation startTransaction, withScope, + withIsolationScope, NodeClient, makeNodeTransport, close, diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts index d37bbbd2023c..fb929737f8d4 100644 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ b/packages/serverless/test/__mocks__/@sentry/node.ts @@ -11,7 +11,6 @@ export const continueTrace = origSentry.continueTrace; export const fakeScope = { addEventProcessor: jest.fn(), - setTransactionName: jest.fn(), setTag: jest.fn(), setContext: jest.fn(), setSpan: jest.fn(), @@ -46,7 +45,6 @@ export const resetMocks = (): void => { fakeSpan.setHttpStatus.mockClear(); fakeScope.addEventProcessor.mockClear(); - fakeScope.setTransactionName.mockClear(); fakeScope.setTag.mockClear(); fakeScope.setContext.mockClear(); fakeScope.setSpan.mockClear(); diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index f2aa18e9cb8d..0da204b31fa4 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -1,4 +1,5 @@ // NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import type { Event } from '@sentry/types'; import type { Callback, Handler } from 'aws-lambda'; @@ -41,7 +42,14 @@ const fakeCallback: Callback = (err, result) => { function expectScopeSettings() { // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTransactionName).toBeCalledWith('functionName'); + expect(SentryNode.fakeScope.addEventProcessor).toBeCalledTimes(1); + // Test than an event processor to add `transaction` is registered for the scope + // @ts-expect-error see "Why @ts-expect-error" note + const eventProcessor = SentryNode.fakeScope.addEventProcessor.mock.calls[0][0]; + const event: Event = {}; + eventProcessor(event); + expect(event).toEqual({ transaction: 'functionName' }); + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledWith('server_name', expect.anything()); // @ts-expect-error see "Why @ts-expect-error" note @@ -185,7 +193,7 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTransactionName).toBeCalledTimes(0); + expect(SentryNode.fakeScope.addEventProcessor).toBeCalledTimes(0); // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledTimes(0); expect(SentryNode.startSpanManual).toBeCalledTimes(0); @@ -194,7 +202,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on sync handler', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = (_event, _context, callback) => { callback(null, 42); @@ -206,7 +214,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(rv).toStrictEqual(42); @@ -218,7 +229,7 @@ describe('AWSLambda', () => { }); test('unsuccessful execution', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('sorry'); const handler: Handler = (_event, _context, callback) => { @@ -233,7 +244,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -274,11 +288,13 @@ describe('AWSLambda', () => { origin: 'auto.function.serverless', name: 'functionName', traceId: '12312012123120121231201212312012', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, metadata: { dynamicSamplingContext: { release: '2.12.1', }, - source: 'component', }, }), expect.any(Function), @@ -292,7 +308,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = (_event, _context, _callback) => { @@ -311,7 +327,10 @@ describe('AWSLambda', () => { traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, - metadata: { dynamicSamplingContext: {}, source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: { dynamicSamplingContext: {} }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -326,7 +345,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on async handler', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = async (_event, _context) => { return 42; @@ -338,7 +357,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(rv).toStrictEqual(42); @@ -361,7 +383,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = async (_event, _context) => { @@ -376,7 +398,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -406,7 +431,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on async handler with a callback method (aka incorrect usage)', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = async (_event, _context, _callback) => { return 42; @@ -418,7 +443,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(rv).toStrictEqual(42); @@ -441,7 +469,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = async (_event, _context, _callback) => { @@ -456,7 +484,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 19a3a2565cdd..f4486415988b 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -2,6 +2,7 @@ import * as domain from 'domain'; import * as SentryNode from '@sentry/node'; import type { Event, Integration } from '@sentry/types'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as Sentry from '../src'; import { wrapCloudEventFunction, wrapEventFunction, wrapHttpFunction } from '../src/gcpfunction'; import type { @@ -111,7 +112,10 @@ describe('GCPFunction', () => { name: 'POST /path', op: 'function.gcp.http', origin: 'auto.function.serverless.gcp_http', - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -141,11 +145,13 @@ describe('GCPFunction', () => { traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, metadata: { dynamicSamplingContext: { release: '2.12.1', }, - source: 'route', }, }; @@ -172,7 +178,10 @@ describe('GCPFunction', () => { traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, - metadata: { dynamicSamplingContext: {}, source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + metadata: { dynamicSamplingContext: {} }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -251,7 +260,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -272,7 +283,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -298,7 +311,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -323,7 +338,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -346,7 +363,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -367,7 +386,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -389,7 +410,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -444,7 +467,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -465,7 +490,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -488,7 +515,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -509,7 +538,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -531,7 +562,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6e0c6b18b97d..aff3a68ab70a 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -29,9 +29,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry/browser": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "magic-string": "^0.30.0" }, "peerDependencies": { diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index e579b453f033..38d68468ffd6 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -3,6 +3,7 @@ import type { Span, Transaction } from '@sentry/types'; import { afterUpdate, beforeUpdate, onMount } from 'svelte'; import { current_component } from 'svelte/internal'; +import { getRootSpan } from '@sentry/core'; import { DEFAULT_COMPONENT_NAME, UI_SVELTE_INIT, UI_SVELTE_UPDATE } from './constants'; import type { TrackComponentOptions } from './types'; @@ -47,6 +48,7 @@ export function trackComponent(options?: TrackComponentOptions): void { } function recordInitSpan(transaction: Transaction, componentName: string): Span { + // eslint-disable-next-line deprecation/deprecation const initSpan = transaction.startChild({ op: UI_SVELTE_INIT, description: componentName, @@ -73,8 +75,9 @@ function recordUpdateSpans(componentName: string, initSpan?: Span): void { // If we are initializing the component when the update span is started, we start it as child // of the init span. Else, we start it as a child of the transaction. const parentSpan = - initSpan && !initSpan.endTimestamp && initSpan.transaction === transaction ? initSpan : transaction; + initSpan && !initSpan.endTimestamp && getRootSpan(initSpan) === transaction ? initSpan : transaction; + // eslint-disable-next-line deprecation/deprecation updateSpan = parentSpan.startChild({ op: UI_SVELTE_UPDATE, description: componentName, @@ -92,5 +95,6 @@ function recordUpdateSpans(componentName: string, initSpan?: Span): void { } function getActiveTransaction(): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getTransaction(); } diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index ebafe765b61b..f074a5915185 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/sveltekit", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for SvelteKit", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit", @@ -37,13 +37,13 @@ "@sveltejs/kit": "1.x || 2.x" }, "dependencies": { - "@sentry-internal/tracing": "7.92.0", - "@sentry/core": "7.92.0", - "@sentry/integrations": "7.92.0", - "@sentry/node": "7.92.0", - "@sentry/svelte": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry-internal/tracing": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/integrations": "7.93.0", + "@sentry/node": "7.93.0", + "@sentry/svelte": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "@sentry/vite-plugin": "^0.6.1", "magicast": "0.2.8", "sorcery": "0.11.0" diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts index 0d327335326b..2b36d4adb4f2 100644 --- a/packages/sveltekit/src/client/router.ts +++ b/packages/sveltekit/src/client/router.ts @@ -1,4 +1,4 @@ -import { getActiveTransaction } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveTransaction } from '@sentry/core'; import { WINDOW } from '@sentry/svelte'; import type { Span, Transaction, TransactionContext } from '@sentry/types'; @@ -43,8 +43,8 @@ function instrumentPageload(startTransactionFn: (context: TransactionContext) => tags: { ...DEFAULT_TAGS, }, - metadata: { - source: 'url', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }); @@ -57,7 +57,7 @@ function instrumentPageload(startTransactionFn: (context: TransactionContext) => if (pageloadTransaction && routeId) { pageloadTransaction.updateName(routeId); - pageloadTransaction.setMetadata({ source: 'route' }); + pageloadTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } }); } @@ -98,6 +98,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) const parameterizedRouteOrigin = from && from.route.id; const parameterizedRouteDestination = to && to.route.id; + // eslint-disable-next-line deprecation/deprecation activeTransaction = getActiveTransaction(); if (!activeTransaction) { @@ -105,7 +106,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) name: parameterizedRouteDestination || rawRouteDestination || 'unknown', op: 'navigation', origin: 'auto.navigation.sveltekit', - metadata: { source: parameterizedRouteDestination ? 'route' : 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedRouteDestination ? 'route' : 'url' }, tags: { ...DEFAULT_TAGS, }, @@ -117,11 +118,13 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) // If a routing span is still open from a previous navigation, we finish it. routingSpan.end(); } + // eslint-disable-next-line deprecation/deprecation routingSpan = activeTransaction.startChild({ op: 'ui.sveltekit.routing', description: 'SvelteKit Route Change', origin: 'auto.ui.sveltekit', }); + // eslint-disable-next-line deprecation/deprecation activeTransaction.setTag('from', parameterizedRouteOrigin); } }); diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 540de96c6de9..1bb0c485168e 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,4 +1,4 @@ -import { getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { getActiveSpan, getCurrentScope, getDynamicSamplingContextFromSpan, spanToTraceHeader } from '@sentry/core'; import { getActiveTransaction, runWithAsyncContext, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ @@ -95,11 +95,12 @@ export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (transaction) { const traceparentData = spanToTraceHeader(transaction); const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader( - transaction.getDynamicSamplingContext(), + getDynamicSamplingContextFromSpan(transaction), ); const contentMeta = ` @@ -142,7 +143,7 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { // if there is an active transaction, we know that this handle call is nested and hence // we don't create a new domain for it. If we created one, nested server calls would // create new transactions instead of adding a child span to the currently active span. - if (getCurrentScope().getSpan()) { + if (getActiveSpan()) { return instrumentHandle(input, options); } return runWithAsyncContext(() => { diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index f5f86f27b7ea..fd90d7e584b7 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -19,6 +19,7 @@ export { createTransport, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -42,6 +43,7 @@ export { // eslint-disable-next-line deprecation/deprecation trace, withScope, + withIsolationScope, autoDiscoverNodePerformanceMonitoringIntegrations, makeNodeTransport, defaultIntegrations, diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts index 648e5344e6c8..29037c28461f 100644 --- a/packages/sveltekit/test/client/router.test.ts +++ b/packages/sveltekit/test/client/router.test.ts @@ -6,6 +6,7 @@ import { vi } from 'vitest'; import { navigating, page } from '$app/stores'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { svelteKitRoutingInstrumentation } from '../../src/client/router'; // we have to overwrite the global mock from `vitest.setup.ts` here to reset the @@ -27,7 +28,7 @@ describe('sveltekitRoutingInstrumentation', () => { returnedTransaction = { ...txnCtx, updateName: vi.fn(), - setMetadata: vi.fn(), + setAttribute: vi.fn(), startChild: vi.fn().mockImplementation(ctx => { return { ...mockedRoutingSpan, ...ctx }; }), @@ -59,8 +60,8 @@ describe('sveltekitRoutingInstrumentation', () => { tags: { 'routing.instrumentation': '@sentry/sveltekit', }, - metadata: { - source: 'url', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }); @@ -71,7 +72,7 @@ describe('sveltekitRoutingInstrumentation', () => { // This should update the transaction name with the parameterized route: expect(returnedTransaction?.updateName).toHaveBeenCalledTimes(1); expect(returnedTransaction?.updateName).toHaveBeenCalledWith('testRoute'); - expect(returnedTransaction?.setMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(returnedTransaction?.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it("doesn't start a pageload transaction if `startTransactionOnPageLoad` is false", () => { @@ -109,20 +110,20 @@ describe('sveltekitRoutingInstrumentation', () => { name: '/users/[id]', op: 'navigation', origin: 'auto.navigation.sveltekit', - metadata: { - source: 'route', - }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' }, tags: { 'routing.instrumentation': '@sentry/sveltekit', }, }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.startChild).toHaveBeenCalledWith({ op: 'ui.sveltekit.routing', origin: 'auto.ui.sveltekit', description: 'SvelteKit Route Change', }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users'); // We emit `null` here to simulate the end of the navigation lifecycle @@ -160,20 +161,20 @@ describe('sveltekitRoutingInstrumentation', () => { name: '/users/[id]', op: 'navigation', origin: 'auto.navigation.sveltekit', - metadata: { - source: 'route', - }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' }, tags: { 'routing.instrumentation': '@sentry/sveltekit', }, }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.startChild).toHaveBeenCalledWith({ op: 'ui.sveltekit.routing', origin: 'auto.ui.sveltekit', description: 'SvelteKit Route Change', }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users/[id]'); }); diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts index fafaa941f642..28f053823e3b 100644 --- a/packages/sveltekit/test/client/sdk.test.ts +++ b/packages/sveltekit/test/client/sdk.test.ts @@ -1,4 +1,4 @@ -import { getClient, getCurrentHub } from '@sentry/core'; +import { getClient, getCurrentScope } from '@sentry/core'; import type { BrowserClient } from '@sentry/svelte'; import * as SentrySvelte from '@sentry/svelte'; import { SDK_VERSION, WINDOW } from '@sentry/svelte'; @@ -39,7 +39,7 @@ describe('Sentry client SDK', () => { }); it('sets the runtime tag on the scope', () => { - const currentScope = getCurrentHub().getScope(); + const currentScope = getCurrentScope(); // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); @@ -62,7 +62,7 @@ describe('Sentry client SDK', () => { }); const integrationsToInit = svelteInit.mock.calls[0][0].integrations; - const browserTracing = getClient()?.getIntegrationById('BrowserTracing'); + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); expect(browserTracing).toBeDefined(); @@ -78,7 +78,7 @@ describe('Sentry client SDK', () => { }); const integrationsToInit = svelteInit.mock.calls[0][0].integrations; - const browserTracing = getClient()?.getIntegrationById('BrowserTracing'); + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); expect(browserTracing).toBeUndefined(); @@ -97,7 +97,7 @@ describe('Sentry client SDK', () => { }); const integrationsToInit = svelteInit.mock.calls[0][0].integrations; - const browserTracing = getClient()?.getIntegrationById('BrowserTracing'); + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); expect(browserTracing).toBeUndefined(); @@ -115,7 +115,7 @@ describe('Sentry client SDK', () => { const integrationsToInit = svelteInit.mock.calls[0][0].integrations; - const browserTracing = getClient()?.getIntegrationById('BrowserTracing') as BrowserTracing; + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing') as BrowserTracing; const options = browserTracing.options; expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); diff --git a/packages/sveltekit/test/server/sdk.test.ts b/packages/sveltekit/test/server/sdk.test.ts index ce936901110d..b4ecf75a744a 100644 --- a/packages/sveltekit/test/server/sdk.test.ts +++ b/packages/sveltekit/test/server/sdk.test.ts @@ -1,4 +1,3 @@ -import { getCurrentHub } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; import { GLOBAL_OBJ } from '@sentry/utils'; @@ -37,7 +36,7 @@ describe('Sentry server SDK', () => { }); it('sets the runtime tag on the scope', () => { - const currentScope = getCurrentHub().getScope(); + const currentScope = SentryNode.getCurrentScope(); // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); diff --git a/packages/tracing-internal/package.json b/packages/tracing-internal/package.json index 9e0b60488b11..6b4b75ed6e18 100644 --- a/packages/tracing-internal/package.json +++ b/packages/tracing-internal/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/tracing", - "version": "7.92.0", + "version": "7.93.0", "description": "Sentry Internal Tracing Package", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tracing-internal", @@ -29,9 +29,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "devDependencies": { "@types/express": "^4.17.14" diff --git a/packages/tracing-internal/src/browser/backgroundtab.ts b/packages/tracing-internal/src/browser/backgroundtab.ts index e13b997b16db..849f20ad20de 100644 --- a/packages/tracing-internal/src/browser/backgroundtab.ts +++ b/packages/tracing-internal/src/browser/backgroundtab.ts @@ -12,6 +12,7 @@ import { WINDOW } from './types'; export function registerBackgroundTabDetection(): void { if (WINDOW && WINDOW.document) { WINDOW.document.addEventListener('visibilitychange', () => { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction() as IdleTransaction; if (WINDOW.document.hidden && activeTransaction) { const statusType: SpanStatusType = 'cancelled'; @@ -25,6 +26,8 @@ export function registerBackgroundTabDetection(): void { if (!activeTransaction.status) { activeTransaction.setStatus(statusType); } + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation activeTransaction.setTag('visibilitychange', 'document.hidden'); activeTransaction.end(); } diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index d52d0e306379..d6ee68302020 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -1,6 +1,13 @@ /* eslint-disable max-lines */ import type { Hub, IdleTransaction } from '@sentry/core'; -import { TRACING_DEFAULTS, addTracingExtensions, getActiveTransaction, startIdleTransaction } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + TRACING_DEFAULTS, + addTracingExtensions, + getActiveTransaction, + spanIsSampled, + startIdleTransaction, +} from '@sentry/core'; import type { EventProcessor, Integration, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getDomElement, logger, tracingContextFromHeaders } from '@sentry/utils'; @@ -223,6 +230,7 @@ export class BrowserTracing implements Integration { public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { this._getCurrentHub = getCurrentHub; const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation const client = hub.getClient(); const clientOptions = client && client.getOptions(); @@ -312,6 +320,7 @@ export class BrowserTracing implements Integration { ...context, ...traceparentData, metadata: { + // eslint-disable-next-line deprecation/deprecation ...context.metadata, dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, }, @@ -325,13 +334,22 @@ export class BrowserTracing implements Integration { const finalContext = modifiedContext === undefined ? { ...expandedContext, sampled: false } : modifiedContext; // If `beforeNavigate` set a custom name, record that fact + // eslint-disable-next-line deprecation/deprecation finalContext.metadata = finalContext.name !== expandedContext.name - ? { ...finalContext.metadata, source: 'custom' } - : finalContext.metadata; + ? // eslint-disable-next-line deprecation/deprecation + { ...finalContext.metadata, source: 'custom' } + : // eslint-disable-next-line deprecation/deprecation + finalContext.metadata; this._latestRouteName = finalContext.name; - this._latestRouteSource = finalContext.metadata && finalContext.metadata.source; + + // eslint-disable-next-line deprecation/deprecation + const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromMetadata = finalContext.metadata && finalContext.metadata.source; + + this._latestRouteSource = sourceFromData || sourceFromMetadata; // eslint-disable-next-line deprecation/deprecation if (finalContext.sampled === false) { @@ -352,6 +370,7 @@ export class BrowserTracing implements Integration { heartbeatInterval, ); + // eslint-disable-next-line deprecation/deprecation const scope = hub.getScope(); // If it's a pageload and there is a meta tag set @@ -362,10 +381,10 @@ export class BrowserTracing implements Integration { // Navigation transactions should set a new propagation context based on the // created idle transaction. scope.setPropagationContext({ - traceId: idleTransaction.traceId, - spanId: idleTransaction.spanId, + traceId: idleTransaction.spanContext().traceId, + spanId: idleTransaction.spanContext().spanId, parentSpanId: idleTransaction.parentSpanId, - sampled: idleTransaction.sampled, + sampled: spanIsSampled(idleTransaction), }); } @@ -384,6 +403,7 @@ export class BrowserTracing implements Integration { const { idleTimeout, finalTimeout, heartbeatInterval } = this.options; const op = 'ui.action.click'; + // eslint-disable-next-line deprecation/deprecation const currentTransaction = getActiveTransaction(); if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) { DEBUG_BUILD && @@ -416,8 +436,8 @@ export class BrowserTracing implements Integration { name: this._latestRouteName, op, trimEnd: true, - metadata: { - source: this._latestRouteSource || 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: this._latestRouteSource || 'url', }, }; diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 651246dfb688..36aa0c3659d9 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ import type { IdleTransaction, Transaction } from '@sentry/core'; -import { getActiveTransaction } from '@sentry/core'; +import { getActiveTransaction, setMeasurement } from '@sentry/core'; import type { Measurements, SpanContext } from '@sentry/types'; import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger } from '@sentry/utils'; @@ -69,6 +69,7 @@ export function startTrackingWebVitals(): () => void { export function startTrackingLongTasks(): void { addPerformanceInstrumentationHandler('longtask', ({ entries }) => { for (const entry of entries) { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction() as IdleTransaction | undefined; if (!transaction) { return; @@ -76,6 +77,7 @@ export function startTrackingLongTasks(): void { const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(entry.duration); + // eslint-disable-next-line deprecation/deprecation transaction.startChild({ description: 'Main UI thread blocked', op: 'ui.long-task', @@ -93,6 +95,7 @@ export function startTrackingLongTasks(): void { export function startTrackingInteractions(): void { addPerformanceInstrumentationHandler('event', ({ entries }) => { for (const entry of entries) { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction() as IdleTransaction | undefined; if (!transaction) { return; @@ -112,9 +115,10 @@ export function startTrackingInteractions(): void { const componentName = getComponentName(entry.target); if (componentName) { - span.data = { 'ui.component_name': componentName }; + span.attributes = { 'ui.component_name': componentName }; } + // eslint-disable-next-line deprecation/deprecation transaction.startChild(span); } } @@ -292,11 +296,7 @@ export function addPerformanceEntries(transaction: Transaction): void { } Object.keys(_measurements).forEach(measurementName => { - transaction.setMeasurement( - measurementName, - _measurements[measurementName].value, - _measurements[measurementName].unit, - ); + setMeasurement(measurementName, _measurements[measurementName].value, _measurements[measurementName].unit); }); _tagMetricInfo(transaction); @@ -444,10 +444,14 @@ function _trackNavigator(transaction: Transaction): void { const connection = navigator.connection; if (connection) { if (connection.effectiveType) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('effectiveConnectionType', connection.effectiveType); } if (connection.type) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('connectionType', connection.type); } @@ -457,10 +461,14 @@ function _trackNavigator(transaction: Transaction): void { } if (isMeasurementValue(navigator.deviceMemory)) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('deviceMemory', `${navigator.deviceMemory} GB`); } if (isMeasurementValue(navigator.hardwareConcurrency)) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('hardwareConcurrency', String(navigator.hardwareConcurrency)); } } @@ -473,18 +481,26 @@ function _tagMetricInfo(transaction: Transaction): void { // Capture Properties of the LCP element that contributes to the LCP. if (_lcpEntry.element) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.element', htmlTreeAsString(_lcpEntry.element)); } if (_lcpEntry.id) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.id', _lcpEntry.id); } if (_lcpEntry.url) { // Trim URL to the first 200 characters. + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.url', _lcpEntry.url.trim().slice(0, 200)); } + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.size', _lcpEntry.size); } @@ -492,6 +508,8 @@ function _tagMetricInfo(transaction: Transaction): void { if (_clsEntry && _clsEntry.sources) { DEBUG_BUILD && logger.log('[Measurements] Adding CLS Data'); _clsEntry.sources.forEach((source, index) => + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), ); } diff --git a/packages/tracing-internal/src/browser/metrics/utils.ts b/packages/tracing-internal/src/browser/metrics/utils.ts index 80bf01b9c333..cebabd9abf1c 100644 --- a/packages/tracing-internal/src/browser/metrics/utils.ts +++ b/packages/tracing-internal/src/browser/metrics/utils.ts @@ -18,6 +18,7 @@ export function _startChild(transaction: Transaction, { startTimestamp, ...ctx } transaction.startTimestamp = startTimestamp; } + // eslint-disable-next-line deprecation/deprecation return transaction.startChild({ startTimestamp, ...ctx, diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index b85d54c96622..461171ec1e25 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -1,9 +1,13 @@ /* eslint-disable max-lines */ import { + getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, + getRootSpan, hasTracingEnabled, + spanToJSON, spanToTraceHeader, } from '@sentry/core'; import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/types'; @@ -145,9 +149,9 @@ function isPerformanceResourceTiming(entry: PerformanceEntry): entry is Performa * @param span A span that has yet to be finished, must contain `url` on data. */ function addHTTPTimings(span: Span): void { - const url = span.data.url; + const { url } = spanToJSON(span).data || {}; - if (!url) { + if (!url || typeof url !== 'string') { return; } @@ -155,7 +159,7 @@ function addHTTPTimings(span: Span): void { entries.forEach(entry => { if (isPerformanceResourceTiming(entry) && entry.name.endsWith(url)) { const spanData = resourceTimingEntryToSpanData(entry); - spanData.forEach(data => span.setData(...data)); + spanData.forEach(data => span.setAttribute(...data)); // In the next tick, clean this handler up // We have to wait here because otherwise this cleans itself up before it is fully done setTimeout(cleanup); @@ -271,11 +275,12 @@ export function xhrCallback( } const scope = getCurrentScope(); - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const span = shouldCreateSpanResult && parentSpan - ? parentSpan.startChild({ + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild({ data: { type: 'xhr', 'http.method': sentryXhrData.method, @@ -288,14 +293,14 @@ export function xhrCallback( : undefined; if (span) { - xhr.__sentry_xhr_span_id__ = span.spanId; + xhr.__sentry_xhr_span_id__ = span.spanContext().spanId; spans[xhr.__sentry_xhr_span_id__] = span; } if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) { if (span) { - const transaction = span && span.transaction; - const dynamicSamplingContext = transaction && transaction.getDynamicSamplingContext(); + const transaction = span && getRootSpan(span); + const dynamicSamplingContext = transaction && getDynamicSamplingContextFromSpan(transaction); const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); setHeaderOnXhr(xhr, spanToTraceHeader(span), sentryBaggageHeader); } else { diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index dfc81bff05f7..e0a8e2ed9fa3 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -1,7 +1,10 @@ import { + getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, + getRootSpan, hasTracingEnabled, spanToTraceHeader, } from '@sentry/core'; @@ -57,7 +60,7 @@ export function instrumentFetchRequest( if (contentLength) { const contentLengthNum = parseInt(contentLength); if (contentLengthNum > 0) { - span.setData('http.response_content_length', contentLengthNum); + span.setAttribute('http.response_content_length', contentLengthNum); } } } else if (handlerData.error) { @@ -73,13 +76,14 @@ export function instrumentFetchRequest( const scope = getCurrentScope(); const client = getClient(); - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const { method, url } = handlerData.fetchData; const span = shouldCreateSpanResult && parentSpan - ? parentSpan.startChild({ + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild({ data: { url, type: 'fetch', @@ -92,8 +96,8 @@ export function instrumentFetchRequest( : undefined; if (span) { - handlerData.fetchData.__span = span.spanId; - spans[span.spanId] = span; + handlerData.fetchData.__span = span.spanContext().spanId; + spans[span.spanContext().spanId] = span; } if (shouldAttachHeaders(handlerData.fetchData.url) && client) { @@ -128,15 +132,16 @@ export function addTracingHeadersToFetchRequest( }, requestSpan?: Span, ): PolymorphicRequestHeaders | undefined { + // eslint-disable-next-line deprecation/deprecation const span = requestSpan || scope.getSpan(); - const transaction = span && span.transaction; + const transaction = span && getRootSpan(span); const { traceId, sampled, dsc } = scope.getPropagationContext(); const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); const dynamicSamplingContext = transaction - ? transaction.getDynamicSamplingContext() + ? getDynamicSamplingContextFromSpan(transaction) : dsc ? dsc : getDynamicSamplingContextFromClient(traceId, client, scope); @@ -144,7 +149,8 @@ export function addTracingHeadersToFetchRequest( const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); const headers = - typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request as Request).headers : options.headers; + options.headers || + (typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request as Request).headers : undefined); if (!headers) { return { 'sentry-trace': sentryTraceHeader, baggage: sentryBaggageHeader }; diff --git a/packages/tracing-internal/src/exports/index.ts b/packages/tracing-internal/src/exports/index.ts index 7b7f81a9caa1..8c10b3165608 100644 --- a/packages/tracing-internal/src/exports/index.ts +++ b/packages/tracing-internal/src/exports/index.ts @@ -1,6 +1,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, hasTracingEnabled, IdleTransaction, diff --git a/packages/tracing-internal/src/node/integrations/apollo.ts b/packages/tracing-internal/src/node/integrations/apollo.ts index f46de137680a..280d958126f7 100644 --- a/packages/tracing-internal/src/node/integrations/apollo.ts +++ b/packages/tracing-internal/src/node/integrations/apollo.ts @@ -188,8 +188,11 @@ function wrapResolver( ): void { fill(model[resolverGroupName], resolverName, function (orig: () => unknown | Promise) { return function (this: unknown, ...args: unknown[]) { + // eslint-disable-next-line deprecation/deprecation const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: `${resolverGroupName}.${resolverName}`, op: 'graphql.resolve', diff --git a/packages/tracing-internal/src/node/integrations/express.ts b/packages/tracing-internal/src/node/integrations/express.ts index 7754fff3ad5b..7dabd973252e 100644 --- a/packages/tracing-internal/src/node/integrations/express.ts +++ b/packages/tracing-internal/src/node/integrations/express.ts @@ -1,4 +1,5 @@ /* eslint-disable max-lines */ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import type { Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types'; import { GLOBAL_OBJ, @@ -157,6 +158,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { return function (this: NodeJS.Global, req: unknown, res: ExpressResponse & SentryTracingResponse): void { const transaction = res.__sentry_transaction; if (transaction) { + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ description: fn.name, op: `middleware.express.${method}`, @@ -177,6 +179,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { next: () => void, ): void { const transaction = res.__sentry_transaction; + // eslint-disable-next-line deprecation/deprecation const span = transaction?.startChild({ description: fn.name, op: `middleware.express.${method}`, @@ -197,6 +200,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { next: () => void, ): void { const transaction = res.__sentry_transaction; + // eslint-disable-next-line deprecation/deprecation const span = transaction?.startChild({ description: fn.name, op: `middleware.express.${method}`, @@ -371,14 +375,15 @@ function instrumentRouter(appOrRouter: ExpressRouter): void { } const transaction = res.__sentry_transaction; - if (transaction && transaction.metadata.source !== 'custom') { + const attributes = (transaction && spanToJSON(transaction).data) || {}; + if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') { // If the request URL is '/' or empty, the reconstructed route will be empty. // Therefore, we fall back to setting the final route to '/' in this case. const finalRoute = req._reconstructedRoute || '/'; const [name, source] = extractPathForTransaction(req, { path: true, method: true, customRoute: finalRoute }); transaction.updateName(name); - transaction.setMetadata({ source }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } } diff --git a/packages/tracing-internal/src/node/integrations/graphql.ts b/packages/tracing-internal/src/node/integrations/graphql.ts index 16773daf49b6..148329dbd86d 100644 --- a/packages/tracing-internal/src/node/integrations/graphql.ts +++ b/packages/tracing-internal/src/node/integrations/graphql.ts @@ -51,15 +51,19 @@ export class GraphQL implements LazyLoadedIntegration { fill(pkg, 'execute', function (orig: () => void | Promise) { return function (this: unknown, ...args: unknown[]) { + // eslint-disable-next-line deprecation/deprecation const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: 'execute', op: 'graphql.execute', origin: 'auto.graphql.graphql', }); + // eslint-disable-next-line deprecation/deprecation scope?.setSpan(span); const rv = orig.call(this, ...args); @@ -67,6 +71,7 @@ export class GraphQL implements LazyLoadedIntegration { if (isThenable(rv)) { return rv.then((res: unknown) => { span?.end(); + // eslint-disable-next-line deprecation/deprecation scope?.setSpan(parentSpan); return res; @@ -74,6 +79,7 @@ export class GraphQL implements LazyLoadedIntegration { } span?.end(); + // eslint-disable-next-line deprecation/deprecation scope?.setSpan(parentSpan); return rv; }; diff --git a/packages/tracing-internal/src/node/integrations/mongo.ts b/packages/tracing-internal/src/node/integrations/mongo.ts index 966231db2f74..61001b94c8cd 100644 --- a/packages/tracing-internal/src/node/integrations/mongo.ts +++ b/packages/tracing-internal/src/node/integrations/mongo.ts @@ -174,12 +174,15 @@ export class Mongo implements LazyLoadedIntegration { fill(collection.prototype, operation, function (orig: () => void | Promise) { return function (this: unknown, ...args: unknown[]) { const lastArg = args[args.length - 1]; + // eslint-disable-next-line deprecation/deprecation const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); // Check if the operation was passed a callback. (mapReduce requires a different check, as // its (non-callback) arguments can also be functions.) if (typeof lastArg !== 'function' || (operation === 'mapReduce' && args.length === 2)) { + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild(getSpanContext(this, operation, args)); const maybePromiseOrCursor = orig.call(this, ...args); @@ -211,6 +214,7 @@ export class Mongo implements LazyLoadedIntegration { } } + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild(getSpanContext(this, operation, args.slice(0, -1))); return orig.call(this, ...args.slice(0, -1), function (err: Error, result: unknown) { diff --git a/packages/tracing-internal/src/node/integrations/mysql.ts b/packages/tracing-internal/src/node/integrations/mysql.ts index c85b0021d89a..285f4988e2f0 100644 --- a/packages/tracing-internal/src/node/integrations/mysql.ts +++ b/packages/tracing-internal/src/node/integrations/mysql.ts @@ -73,7 +73,7 @@ export class Mysql implements LazyLoadedIntegration { DEBUG_BUILD && logger.error('Mysql Integration was unable to instrument `mysql` config.'); } - function spanDataFromConfig(): Record { + function spanDataFromConfig(): Record { if (!mySqlConfig) { return {}; } @@ -91,7 +91,7 @@ export class Mysql implements LazyLoadedIntegration { const data = spanDataFromConfig(); Object.keys(data).forEach(key => { - span.setData(key, data[key]); + span.setAttribute(key, data[key]); }); span.end(); @@ -103,9 +103,12 @@ export class Mysql implements LazyLoadedIntegration { // function (options, values, callback) => void fill(pkg, 'createQuery', function (orig: () => void) { return function (this: unknown, options: unknown, values: unknown, callback: unknown) { + // eslint-disable-next-line deprecation/deprecation const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: typeof options === 'string' ? options : (options as { sql: string }).sql, op: 'db', diff --git a/packages/tracing-internal/src/node/integrations/postgres.ts b/packages/tracing-internal/src/node/integrations/postgres.ts index 810f07825653..086cebf84ee5 100644 --- a/packages/tracing-internal/src/node/integrations/postgres.ts +++ b/packages/tracing-internal/src/node/integrations/postgres.ts @@ -104,7 +104,9 @@ export class Postgres implements LazyLoadedIntegration { */ fill(Client.prototype, 'query', function (orig: PgClientQuery) { return function (this: PgClientThis, config: unknown, values: unknown, callback: unknown) { + // eslint-disable-next-line deprecation/deprecation const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); const data: Record = { @@ -128,6 +130,7 @@ export class Postgres implements LazyLoadedIntegration { // ignore } + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: typeof config === 'string' ? config : (config as { text: string }).text, op: 'db', diff --git a/packages/tracing-internal/src/node/integrations/utils/node-utils.ts b/packages/tracing-internal/src/node/integrations/utils/node-utils.ts index 11f8f2430b61..cc40f2946406 100644 --- a/packages/tracing-internal/src/node/integrations/utils/node-utils.ts +++ b/packages/tracing-internal/src/node/integrations/utils/node-utils.ts @@ -7,6 +7,7 @@ import type { Hub } from '@sentry/types'; * @returns boolean */ export function shouldDisableAutoInstrumentation(getCurrentHub: () => Hub): boolean { + // eslint-disable-next-line deprecation/deprecation const clientOptions = getCurrentHub().getClient()?.getOptions(); const instrumenter = clientOptions?.instrumenter || 'sentry'; diff --git a/packages/tracing-internal/test/browser/backgroundtab.test.ts b/packages/tracing-internal/test/browser/backgroundtab.test.ts index 704f3ba89b1c..215a9c5d6583 100644 --- a/packages/tracing-internal/test/browser/backgroundtab.test.ts +++ b/packages/tracing-internal/test/browser/backgroundtab.test.ts @@ -30,6 +30,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { afterEach(() => { events = {}; + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(undefined); }); @@ -54,6 +55,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { events.visibilitychange(); expect(span?.status).toBe('cancelled'); + // eslint-disable-next-line deprecation/deprecation expect(span?.tags.visibilitychange).toBe('document.hidden'); expect(span?.endTimestamp).toBeDefined(); }); diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 20444c7bf4c8..f24b6ce4b45a 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -3,8 +3,10 @@ import type { ResourceEntry } from '../../../src/browser/metrics'; import { _addMeasureSpans, _addResourceSpans } from '../../../src/browser/metrics'; describe('_addMeasureSpans', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ op: 'pageload', name: '/' }); beforeEach(() => { + // eslint-disable-next-line deprecation/deprecation transaction.startChild = jest.fn(); }); @@ -21,12 +23,12 @@ describe('_addMeasureSpans', () => { const startTime = 23; const duration = 356; - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); _addMeasureSpans(transaction, entry, startTime, duration, timeOrigin); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith({ description: 'measure-1', startTimestamp: timeOrigin + startTime, @@ -38,8 +40,10 @@ describe('_addMeasureSpans', () => { }); describe('_addResourceSpans', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ op: 'pageload', name: '/' }); beforeEach(() => { + // eslint-disable-next-line deprecation/deprecation transaction.startChild = jest.fn(); }); @@ -54,7 +58,7 @@ describe('_addResourceSpans', () => { }; _addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); }); @@ -68,7 +72,7 @@ describe('_addResourceSpans', () => { }; _addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); }); @@ -87,9 +91,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', startTime, duration, timeOrigin); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith({ data: { ['http.decoded_response_content_length']: entry.decodedBodySize, @@ -135,7 +139,7 @@ describe('_addResourceSpans', () => { }; _addResourceSpans(transaction, entry, '/assets/to/me', 123, 234, 465); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ op, @@ -155,9 +159,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ data: { @@ -180,9 +184,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ data: {}, @@ -202,9 +206,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ data: {}, diff --git a/packages/tracing-internal/test/browser/metrics/utils.test.ts b/packages/tracing-internal/test/browser/metrics/utils.test.ts index 25f03c11af0b..ae614abc41a6 100644 --- a/packages/tracing-internal/test/browser/metrics/utils.test.ts +++ b/packages/tracing-internal/test/browser/metrics/utils.test.ts @@ -1,8 +1,10 @@ +import { spanToJSON } from '@sentry/core'; import { Span, Transaction } from '../../../src'; import { _startChild } from '../../../src/browser/metrics/utils'; describe('_startChild()', () => { it('creates a span with given properties', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'test' }); const span = _startChild(transaction, { description: 'evaluation', @@ -10,11 +12,12 @@ describe('_startChild()', () => { }); expect(span).toBeInstanceOf(Span); - expect(span.description).toBe('evaluation'); + expect(spanToJSON(span).description).toBe('evaluation'); expect(span.op).toBe('script'); }); it('adjusts the start timestamp if child span starts before transaction', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'test', startTimestamp: 123 }); const span = _startChild(transaction, { description: 'script.js', @@ -27,6 +30,7 @@ describe('_startChild()', () => { }); it('does not adjust start timestamp if child span starts after transaction', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'test', startTimestamp: 123 }); const span = _startChild(transaction, { description: 'script.js', diff --git a/packages/tracing-internal/test/browser/request.test.ts b/packages/tracing-internal/test/browser/request.test.ts index 0f3ce191278a..3dabe104c8f6 100644 --- a/packages/tracing-internal/test/browser/request.test.ts +++ b/packages/tracing-internal/test/browser/request.test.ts @@ -256,7 +256,7 @@ describe('callbacks', () => { expect(finishedSpan).toBeDefined(); expect(finishedSpan).toBeInstanceOf(Span); - expect(finishedSpan.data).toEqual({ + expect(sentryCore.spanToJSON(finishedSpan).data).toEqual({ 'http.response_content_length': 123, 'http.method': 'GET', 'http.response.status_code': 404, diff --git a/packages/tracing/package.json b/packages/tracing/package.json index 40bce888eec1..b7949f48e0df 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tracing", - "version": "7.92.0", + "version": "7.93.0", "description": "Sentry Performance Monitoring Package", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tracing", @@ -29,14 +29,14 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "7.92.0" + "@sentry-internal/tracing": "7.93.0" }, "devDependencies": { - "@sentry-internal/integration-shims": "7.92.0", - "@sentry/browser": "7.92.0", - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0", + "@sentry-internal/integration-shims": "7.93.0", + "@sentry/browser": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", "@types/express": "^4.17.14" }, "scripts": { diff --git a/packages/tracing/src/index.ts b/packages/tracing/src/index.ts index a515db240117..8559188884d7 100644 --- a/packages/tracing/src/index.ts +++ b/packages/tracing/src/index.ts @@ -62,6 +62,7 @@ export const addExtensionMethods = addExtensionMethodsT; * * `getActiveTransaction` can be imported from `@sentry/node`, `@sentry/browser`, or your framework SDK */ +// eslint-disable-next-line deprecation/deprecation export const getActiveTransaction = getActiveTransactionT; /** diff --git a/packages/tracing/test/hub.test.ts b/packages/tracing/test/hub.test.ts index f7cf93cc5e32..a7fbf5e86b62 100644 --- a/packages/tracing/test/hub.test.ts +++ b/packages/tracing/test/hub.test.ts @@ -1,7 +1,7 @@ /* eslint-disable deprecation/deprecation */ /* eslint-disable @typescript-eslint/unbound-method */ import { BrowserClient } from '@sentry/browser'; -import { Hub, makeMain } from '@sentry/core'; +import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, makeMain } from '@sentry/core'; import * as utilsModule from '@sentry/utils'; // for mocking import { logger } from '@sentry/utils'; @@ -16,7 +16,7 @@ import { addExtensionMethods(); const mathRandom = jest.spyOn(Math, 'random'); -jest.spyOn(Transaction.prototype, 'setMetadata'); +jest.spyOn(Transaction.prototype, 'setAttribute'); jest.spyOn(logger, 'warn'); jest.spyOn(logger, 'log'); jest.spyOn(logger, 'error'); @@ -286,9 +286,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark', sampled: true }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledWith({ - sampleRate: 1.0, - }); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 1); }); it('should record sampling method and rate when sampling decision comes from tracesSampler', () => { @@ -298,9 +296,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark' }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledWith({ - sampleRate: 0.1121, - }); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.1121); }); it('should record sampling method when sampling decision is inherited', () => { @@ -309,7 +305,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark', parentSampled: true }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledTimes(0); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledTimes(0); }); it('should record sampling method and rate when sampling decision comes from traceSampleRate', () => { @@ -318,9 +314,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark' }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledWith({ - sampleRate: 0.1121, - }); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.1121); }); }); diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index e0c5dd189cff..e4a84ccec52c 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -1,7 +1,15 @@ +/* eslint-disable deprecation/deprecation */ import { BrowserClient } from '@sentry/browser'; -import { TRACING_DEFAULTS, Transaction } from '@sentry/core'; - -import { Hub, IdleTransaction, Span } from '../../core/src'; +import { + TRACING_DEFAULTS, + Transaction, + getCurrentScope, + startInactiveSpan, + startSpan, + startSpanManual, +} from '@sentry/core'; + +import { Hub, IdleTransaction, Span, getClient, makeMain } from '../../core/src'; import { IdleTransactionSpanRecorder } from '../../core/src/tracing/idletransaction'; import { getDefaultBrowserClientOptions } from './testutils'; @@ -10,6 +18,7 @@ let hub: Hub; beforeEach(() => { const options = getDefaultBrowserClientOptions({ dsn, tracesSampleRate: 1 }); hub = new Hub(new BrowserClient(options)); + makeMain(hub); }); describe('IdleTransaction', () => { @@ -25,7 +34,8 @@ describe('IdleTransaction', () => { ); transaction.initSpanRecorder(10); - const scope = hub.getScope(); + const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(transaction); }); @@ -33,7 +43,8 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub); transaction.initSpanRecorder(10); - const scope = hub.getScope(); + const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(undefined); }); @@ -51,7 +62,8 @@ describe('IdleTransaction', () => { transaction.end(); jest.runAllTimers(); - const scope = hub.getScope(); + const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(undefined); }); @@ -68,7 +80,8 @@ describe('IdleTransaction', () => { transaction.end(); jest.runAllTimers(); - const scope = hub.getScope(); + const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(undefined); }); @@ -84,13 +97,16 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); // @ts-expect-error need to pass in hub + // eslint-disable-next-line deprecation/deprecation const otherTransaction = new Transaction({ name: 'bar' }, hub); - hub.getScope().setSpan(otherTransaction); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(otherTransaction); transaction.end(); jest.runAllTimers(); - const scope = hub.getScope(); + const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(otherTransaction); }); }); @@ -104,9 +120,11 @@ describe('IdleTransaction', () => { const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild(); - expect(transaction.activities).toMatchObject({ [span.spanId]: true }); + const span = startInactiveSpan({ name: 'inner' })!; + expect(transaction.activities).toMatchObject({ [span.spanContext().spanId]: true }); expect(mockFinish).toHaveBeenCalledTimes(0); @@ -121,8 +139,10 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - transaction.startChild({ startTimestamp: 1234, endTimestamp: 5678 }); + startInactiveSpan({ name: 'inner', startTimestamp: 1234, endTimestamp: 5678 }); expect(transaction.activities).toMatchObject({}); }); @@ -131,16 +151,21 @@ describe('IdleTransaction', () => { const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); + + startSpanManual({ name: 'inner1' }, span => { + const childSpan = startInactiveSpan({ name: 'inner2' })!; + expect(transaction.activities).toMatchObject({ + [span!.spanContext().spanId]: true, + [childSpan.spanContext().spanId]: true, + }); + span?.end(); + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); - const span = transaction.startChild(); - const childSpan = span.startChild(); - - expect(transaction.activities).toMatchObject({ [span.spanId]: true, [childSpan.spanId]: true }); - span.end(); - jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); - - expect(mockFinish).toHaveBeenCalledTimes(0); - expect(transaction.activities).toMatchObject({ [childSpan.spanId]: true }); + expect(mockFinish).toHaveBeenCalledTimes(0); + expect(transaction.activities).toMatchObject({ [childSpan.spanContext().spanId]: true }); + }); }); it('calls beforeFinish callback before finishing', () => { @@ -150,12 +175,13 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); transaction.registerBeforeFinishCallback(mockCallback1); transaction.registerBeforeFinishCallback(mockCallback2); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); expect(mockCallback1).toHaveBeenCalledTimes(0); expect(mockCallback2).toHaveBeenCalledTimes(0); - const span = transaction.startChild(); - span.end(); + startSpan({ name: 'inner' }, () => {}); jest.runOnlyPendingTimers(); expect(mockCallback1).toHaveBeenCalledTimes(1); @@ -167,15 +193,17 @@ describe('IdleTransaction', () => { it('filters spans on finish', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); // regular child - should be kept - const regularSpan = transaction.startChild({ startTimestamp: transaction.startTimestamp + 2 }); + const regularSpan = startInactiveSpan({ name: 'span1', startTimestamp: transaction.startTimestamp + 2 })!; // discardedSpan - startTimestamp is too large - transaction.startChild({ startTimestamp: 645345234 }); + startInactiveSpan({ name: 'span2', startTimestamp: 645345234 }); // Should be cancelled - will not finish - const cancelledSpan = transaction.startChild({ startTimestamp: transaction.startTimestamp + 4 }); + const cancelledSpan = startInactiveSpan({ name: 'span3', startTimestamp: transaction.startTimestamp + 4 })!; regularSpan.end(regularSpan.startTimestamp + 4); transaction.end(transaction.startTimestamp + 10); @@ -184,14 +212,14 @@ describe('IdleTransaction', () => { if (transaction.spanRecorder) { const spans = transaction.spanRecorder.spans; expect(spans).toHaveLength(3); - expect(spans[0].spanId).toBe(transaction.spanId); + expect(spans[0].spanContext().spanId).toBe(transaction.spanContext().spanId); // Regular Span - should not modified - expect(spans[1].spanId).toBe(regularSpan.spanId); + expect(spans[1].spanContext().spanId).toBe(regularSpan.spanContext().spanId); expect(spans[1].endTimestamp).not.toBe(transaction.endTimestamp); // Cancelled Span - has endtimestamp of transaction - expect(spans[2].spanId).toBe(cancelledSpan.spanId); + expect(spans[2].spanContext().spanId).toBe(cancelledSpan.spanContext().spanId); expect(spans[2].status).toBe('cancelled'); expect(spans[2].endTimestamp).toBe(transaction.endTimestamp); } @@ -200,8 +228,10 @@ describe('IdleTransaction', () => { it('filters out spans that exceed final timeout', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, 1000, 3000); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({ startTimestamp: transaction.startTimestamp + 2 }); + const span = startInactiveSpan({ name: 'span', startTimestamp: transaction.startTimestamp + 2 })!; span.end(span.startTimestamp + 10 + 30 + 1); transaction.end(transaction.startTimestamp + 50); @@ -213,7 +243,7 @@ describe('IdleTransaction', () => { it('should record dropped transactions', async () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234, sampled: false }, hub, 1000); - const client = hub.getClient()!; + const client = getClient()!; const recordDroppedEventSpy = jest.spyOn(client, 'recordDroppedEvent'); @@ -235,7 +265,10 @@ describe('IdleTransaction', () => { it('does not finish if a activity is started', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); - transaction.startChild({}); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); + + startInactiveSpan({ name: 'span' }); jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); expect(transaction.endTimestamp).toBeUndefined(); @@ -245,14 +278,14 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span1' }, () => {}); jest.advanceTimersByTime(2); - const span2 = transaction.startChild({}); - span2.end(); + startSpan({ name: 'span2' }, () => {}); jest.advanceTimersByTime(8); @@ -263,14 +296,14 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span1' }, () => {}); jest.advanceTimersByTime(2); - const span2 = transaction.startChild({}); - span2.end(); + startSpan({ name: 'span2' }, () => {}); jest.advanceTimersByTime(10); @@ -283,10 +316,12 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const firstSpan = transaction.startChild({}); + const firstSpan = startInactiveSpan({ name: 'span1' })!; transaction.cancelIdleTimeout(undefined, { restartOnChildSpanChange: false }); - const secondSpan = transaction.startChild({}); + const secondSpan = startInactiveSpan({ name: 'span2' })!; firstSpan.end(); secondSpan.end(); @@ -297,11 +332,13 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const firstSpan = transaction.startChild({}); + const firstSpan = startInactiveSpan({ name: 'span1' })!; transaction.cancelIdleTimeout(undefined, { restartOnChildSpanChange: false }); - const secondSpan = transaction.startChild({}); - const thirdSpan = transaction.startChild({}); + const secondSpan = startInactiveSpan({ name: 'span2' })!; + const thirdSpan = startInactiveSpan({ name: 'span3' })!; firstSpan.end(); expect(transaction.endTimestamp).toBeUndefined(); @@ -317,9 +354,10 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span' }, () => {}); jest.advanceTimersByTime(2); @@ -332,16 +370,16 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span' }, () => {}); jest.advanceTimersByTime(2); transaction.cancelIdleTimeout(); - const span2 = transaction.startChild({}); - span2.end(); + startSpan({ name: 'span' }, () => {}); jest.advanceTimersByTime(8); expect(transaction.endTimestamp).toBeUndefined(); @@ -380,9 +418,11 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub, TRACING_DEFAULTS.idleTimeout); const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); expect(mockFinish).toHaveBeenCalledTimes(0); - transaction.startChild({}); + startInactiveSpan({ name: 'span' }); // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); @@ -401,15 +441,17 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub, TRACING_DEFAULTS.idleTimeout, 50000); const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); expect(mockFinish).toHaveBeenCalledTimes(0); - transaction.startChild({}); + startInactiveSpan({ name: 'span' }); // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); expect(mockFinish).toHaveBeenCalledTimes(0); - const span = transaction.startChild(); // push activity + const span = startInactiveSpan({ name: 'span' })!; // push activity // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); @@ -419,8 +461,8 @@ describe('IdleTransaction', () => { jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); expect(mockFinish).toHaveBeenCalledTimes(0); - transaction.startChild(); // push activity - transaction.startChild(); // push activity + startInactiveSpan({ name: 'span' }); // push activity + startInactiveSpan({ name: 'span' }); // push activity // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); @@ -466,13 +508,13 @@ describe('IdleTransactionSpanRecorder', () => { expect(spanRecorder.spans).toHaveLength(1); expect(mockPushActivity).toHaveBeenCalledTimes(1); - expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanId); + expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanContext().spanId); expect(mockPopActivity).toHaveBeenCalledTimes(0); span.end(); expect(mockPushActivity).toHaveBeenCalledTimes(1); expect(mockPopActivity).toHaveBeenCalledTimes(1); - expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanId); + expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanContext().spanId); }); it('does not push activities if a span has a timestamp', () => { @@ -491,7 +533,12 @@ describe('IdleTransactionSpanRecorder', () => { const mockPopActivity = jest.fn(); const transaction = new IdleTransaction({ name: 'foo' }, hub); - const spanRecorder = new IdleTransactionSpanRecorder(mockPushActivity, mockPopActivity, transaction.spanId, 10); + const spanRecorder = new IdleTransactionSpanRecorder( + mockPushActivity, + mockPopActivity, + transaction.spanContext().spanId, + 10, + ); spanRecorder.add(transaction); expect(mockPushActivity).toHaveBeenCalledTimes(0); diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index 011af5ba10d5..e2ebf335ecb5 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -1,6 +1,6 @@ /* eslint-disable deprecation/deprecation */ import { BrowserClient } from '@sentry/browser'; -import { Hub, Scope, makeMain } from '@sentry/core'; +import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Scope, makeMain } from '@sentry/core'; import type { BaseTransportOptions, ClientOptions, TransactionSource } from '@sentry/types'; import { Span, TRACEPARENT_REGEXP, Transaction } from '../src'; @@ -506,8 +506,8 @@ describe('Span', () => { sampled: true, }); - expect(span.traceId).toBe('c'); - expect(span.spanId).toBe('d'); + expect(span.spanContext().traceId).toBe('c'); + expect(span.spanContext().spanId).toBe('d'); expect(span.sampled).toBe(true); expect(span.description).toBe(undefined); expect(span.op).toBe(undefined); @@ -541,8 +541,8 @@ describe('Span', () => { span.updateWithContext(newContext); - expect(span.traceId).toBe('a'); - expect(span.spanId).toBe('b'); + expect(span.spanContext().traceId).toBe('a'); + expect(span.spanContext().spanId).toBe('b'); expect(span.description).toBe('new'); expect(span.endTimestamp).toBe(1); expect(span.op).toBe('new-op'); @@ -571,24 +571,19 @@ describe('Span', () => { hub, ); - const hubSpy = jest.spyOn(hub.getClient()!, 'getOptions'); - const dynamicSamplingContext = transaction.getDynamicSamplingContext(); - expect(hubSpy).not.toHaveBeenCalled(); expect(dynamicSamplingContext).toStrictEqual({ environment: 'myEnv' }); }); test('should return new DSC, if no DSC was provided during transaction creation', () => { - const transaction = new Transaction( - { - name: 'tx', - metadata: { - sampleRate: 0.56, - }, + const transaction = new Transaction({ + name: 'tx', + metadata: { + sampleRate: 0.56, }, - hub, - ); + sampled: true, + }); const getOptionsSpy = jest.spyOn(hub.getClient()!, 'getOptions'); @@ -598,6 +593,7 @@ describe('Span', () => { expect(dynamicSamplingContext).toStrictEqual({ release: '1.0.1', environment: 'production', + sampled: 'true', sample_rate: '0.56', trace_id: expect.any(String), transaction: 'tx', @@ -645,9 +641,7 @@ describe('Span', () => { test('is included when transaction metadata is set', () => { const spy = jest.spyOn(hub as any, 'captureEvent') as any; const transaction = hub.startTransaction({ name: 'test', sampled: true }); - transaction.setMetadata({ - source: 'url', - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); expect(spy).toHaveBeenCalledTimes(0); transaction.end(); diff --git a/packages/tracing/test/transaction.test.ts b/packages/tracing/test/transaction.test.ts index 12c6c799883a..3d048c9a3c3f 100644 --- a/packages/tracing/test/transaction.test.ts +++ b/packages/tracing/test/transaction.test.ts @@ -1,5 +1,6 @@ /* eslint-disable deprecation/deprecation */ import { BrowserClient, Hub } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { Transaction, addExtensionMethods } from '../src'; import { getDefaultBrowserClientOptions } from './testutils'; @@ -65,7 +66,7 @@ describe('`Transaction` class', () => { describe('`updateName` method', () => { it('does not change the source', () => { const transaction = new Transaction({ name: 'dogpark' }); - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); transaction.updateName('ballpit'); expect(transaction.name).toEqual('ballpit'); @@ -162,6 +163,7 @@ describe('`Transaction` class', () => { contexts: { foo: { key: 'val' }, trace: { + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1 }, span_id: transaction.spanId, trace_id: transaction.traceId, origin: 'manual', @@ -189,6 +191,7 @@ describe('`Transaction` class', () => { expect.objectContaining({ contexts: { trace: { + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1 }, span_id: transaction.spanId, trace_id: transaction.traceId, origin: 'manual', diff --git a/packages/types/package.json b/packages/types/package.json index 45cd65e48fe8..1478b2fe9e55 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "7.92.0", + "version": "7.93.0", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index c9c37349306d..d8d09ec1431b 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -138,9 +138,15 @@ export interface Client { */ getEventProcessors?(): EventProcessor[]; - /** Returns the client's instance of the given integration class, it any. */ + /** + * Returns the client's instance of the given integration class, it any. + * @deprecated Use `getIntegrationByName()` instead. + */ getIntegration(integration: IntegrationClass): T | null; + /** Get the instance of the integration with the given name on the client, if it was added. */ + getIntegrationByName?(name: string): T | undefined; + /** * Add an integration to the client. * This can be used to e.g. lazy load integrations. @@ -151,9 +157,18 @@ export interface Client { * */ addIntegration?(integration: Integration): void; - /** This is an internal function to setup all integrations that should run on the client */ + /** + * This is an internal function to setup all integrations that should run on the client. + * @deprecated Use `client.init()` instead. + */ setupIntegrations(forceInitialize?: boolean): void; + /** + * Initialize this client. + * Call this after the client was set on a scope. + */ + init?(): void; + /** Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any eventFromException(exception: any, hint?: EventHint): PromiseLike; diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 8dadb959a97d..4f92ff4c1c6a 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -95,7 +95,6 @@ export interface ResponseContext extends Record { export interface TraceContext extends Record { data?: { [key: string]: any }; - description?: string; op?: string; parent_span_id?: string; span_id: string; diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index 17e839f3f46e..014738c84e83 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -21,7 +21,7 @@ export interface Hub { * @param version A version number to compare to. * @return True if the given version is newer; otherwise false. * - * @hidden + * @deprecated This will be removed in v8. */ isOlderThan(version: number): boolean; @@ -68,18 +68,28 @@ export interface Hub { * popScope(); * * @param callback that will be enclosed into push/popScope. + * + * @deprecated Use `Sentry.withScope()` instead. */ withScope(callback: (scope: Scope) => T): T; - /** Returns the client of the top stack. */ + /** + * Returns the client of the top stack. + * @deprecated Use `Sentry.getClient()` instead. + */ getClient(): Client | undefined; - /** Returns the scope of the top stack */ + /** + * Returns the scope of the top stack. + * @deprecated Use `Sentry.getCurrentScope()` instead. + */ getScope(): Scope; /** * Get the currently active isolation scope. * The isolation scope is used to isolate data between different hubs. + * + * @deprecated Use `Sentry.getIsolationScope()` instead. */ getIsolationScope(): Scope; @@ -89,6 +99,8 @@ export interface Hub { * @param exception An exception-like object. * @param hint May contain additional information about the original exception. * @returns The generated eventId. + * + * @deprecated Use `Sentry.captureException()` instead. */ captureException(exception: any, hint?: EventHint): string; @@ -99,6 +111,8 @@ export interface Hub { * @param level Define the level of the message. * @param hint May contain additional information about the original exception. * @returns The generated eventId. + * + * @deprecated Use `Sentry.captureMessage()` instead. */ captureMessage( message: string, @@ -112,6 +126,8 @@ export interface Hub { * * @param event The event to send to Sentry. * @param hint May contain additional information about the original exception. + * + * @deprecated Use `Sentry.captureEvent()` instead. */ captureEvent(event: Event, hint?: EventHint): string; @@ -119,6 +135,8 @@ export interface Hub { * This is the getter for lastEventId. * * @returns The last event id of a captured event. + * + * @deprecated This will be removed in v8. */ lastEventId(): string | undefined; @@ -130,6 +148,8 @@ export interface Hub { * * @param breadcrumb The breadcrumb to record. * @param hint May contain additional information about the original breadcrumb. + * + * @deprecated Use `Sentry.addBreadcrumb()` instead. */ addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void; @@ -137,6 +157,8 @@ export interface Hub { * Updates user context information for future events. * * @param user User context object to be set in the current context. Pass `null` to unset the user. + * + * @deprecated Use `Sentry.setUser()` instead. */ setUser(user: User | null): void; @@ -144,6 +166,8 @@ export interface Hub { * Set an object that will be merged sent as tags data with the event. * * @param tags Tags context object to merge into current context. + * + * @deprecated Use `Sentry.setTags()` instead. */ setTags(tags: { [key: string]: Primitive }): void; @@ -154,6 +178,8 @@ export interface Hub { * * @param key String key of tag * @param value Value of tag + * + * @deprecated Use `Sentry.setTag()` instead. */ setTag(key: string, value: Primitive): void; @@ -161,12 +187,16 @@ export interface Hub { * Set key:value that will be sent as extra data with the event. * @param key String of extra * @param extra Any kind of data. This data will be normalized. + * + * @deprecated Use `Sentry.setExtra()` instead. */ setExtra(key: string, extra: Extra): void; /** * Set an object that will be merged sent as extra data with the event. * @param extras Extras object to merge into current context. + * + * @deprecated Use `Sentry.setExtras()` instead. */ setExtras(extras: Extras): void; @@ -174,6 +204,8 @@ export interface Hub { * Sets context data with the given name. * @param name of the context * @param context Any kind of data. This data will be normalized. + * + * @deprecated Use `Sentry.setContext()` instead. */ setContext(name: string, context: { [key: string]: any } | null): void; @@ -189,13 +221,23 @@ export interface Hub { * For the duration of the callback, this hub will be set as the global current Hub. * This function is useful if you want to run your own client and hook into an already initialized one * e.g.: Reporting issues to your own sentry when running in your component while still using the users configuration. + * + * TODO v8: This will be merged with `withScope()` */ run(callback: (hub: Hub) => void): void; - /** Returns the integration if installed on the current client. */ + /** + * Returns the integration if installed on the current client. + * + * @deprecated Use `Sentry.getClient().getIntegration()` instead. + */ getIntegration(integration: IntegrationClass): T | null; - /** Returns all trace headers that are currently on the top scope. */ + /** + * Returns all trace headers that are currently on the top scope. + * + * @deprecated Use `spanToTraceHeader()` instead. + */ traceHeaders(): { [key: string]: string }; /** @@ -230,22 +272,29 @@ export interface Hub { * @param context Optional properties of the new `Session`. * * @returns The session which was just started + * + * @deprecated Use top-level `startSession` instead. */ startSession(context?: Session): Session; /** * Ends the session that lives on the current scope and sends it to Sentry + * + * @deprecated Use top-level `endSession` instead. */ endSession(): void; /** * Sends the current session on the scope to Sentry + * * @param endSession If set the session will be marked as exited and removed from the scope + * + * @deprecated Use top-level `captureSession` instead. */ captureSession(endSession?: boolean): void; /** - * Returns if default PII should be sent to Sentry and propagated in ourgoing requests + * Returns if default PII should be sent to Sentry and propagated in outgoing requests * when Tracing is used. * * @deprecated Use top-level `getClient().getOptions().sendDefaultPii` instead. This function diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a6d259870714..34f668275c94 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -89,7 +89,17 @@ export type { // eslint-disable-next-line deprecation/deprecation export type { Severity, SeverityLevel } from './severity'; -export type { Span, SpanContext, SpanOrigin, SpanAttributeValue, SpanAttributes, SpanTimeInput } from './span'; +export type { + Span, + SpanContext, + SpanOrigin, + SpanAttributeValue, + SpanAttributes, + SpanTimeInput, + SpanJSON, + SpanContextData, + TraceFlag, +} from './span'; export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; export type { TextEncoderInternal } from './textencoder'; diff --git a/packages/types/src/integration.ts b/packages/types/src/integration.ts index a4108a60c749..44c49ab375aa 100644 --- a/packages/types/src/integration.ts +++ b/packages/types/src/integration.ts @@ -13,12 +13,8 @@ export interface IntegrationClass { new (...args: any[]): T; } -/** - * An integration in function form. - * This is expected to return an integration result, - */ -export type IntegrationFn = (...rest: any[]) => IntegrationFnResult; - +/** Integration interface. + * This is more or less the same as `Integration`, but with a slimmer `setupOnce` siganture. */ export interface IntegrationFnResult { /** * The name of the integration. @@ -28,8 +24,10 @@ export interface IntegrationFnResult { /** * This hook is only called once, even if multiple clients are created. * It does not receives any arguments, and should only use for e.g. global monkey patching and similar things. + * + * NOTE: In v8, this will become optional. */ - setupOnce?(): void; + setupOnce(): void; /** * Set up an integration for the given client. @@ -54,16 +52,24 @@ export interface IntegrationFnResult { processEvent?(event: Event, hint: EventHint, client: Client): Event | null | PromiseLike; } +/** + * An integration in function form. + * This is expected to return an integration. + */ +export type IntegrationFn = (...rest: any[]) => IntegrationFnResult; + /** Integration interface */ export interface Integration { /** - * Returns {@link IntegrationClass.id} + * The name of the integration. */ name: string; /** - * Sets the integration up only once. - * This takes no options on purpose, options should be passed in the constructor + * This hook is only called once, even if multiple clients are created. + * It does not receives any arguments, and should only use for e.g. global monkey patching and similar things. + * + * NOTE: In v8, this will become optional, and not receive any arguments anymore. */ setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void; diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 5dbdee05260d..fd51eae8e5c4 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -2,6 +2,7 @@ import type { Attachment } from './attachment'; import type { Breadcrumb } from './breadcrumb'; import type { Client } from './client'; import type { Context, Contexts } from './context'; +import type { Event, EventHint } from './event'; import type { EventProcessor } from './eventprocessor'; import type { Extra, Extras } from './extra'; import type { Primitive } from './misc'; @@ -40,6 +41,7 @@ export interface ScopeData { sdkProcessingMetadata: { [key: string]: unknown }; fingerprint: string[]; level?: SeverityLevel; + /** @deprecated This will be removed in v8. */ transactionName?: string; span?: Span; } @@ -124,6 +126,7 @@ export interface Scope { /** * Sets the transaction name on the scope for future events. + * @deprecated Use extra or tags instead. */ setTransactionName(name?: string): this; @@ -137,16 +140,19 @@ export interface Scope { /** * Sets the Span on the scope. * @param span Span + * @deprecated Instead of setting a span on a scope, use `startSpan()`/`startSpanManual()` instead. */ setSpan(span?: Span): this; /** - * Returns the `Span` if there is one + * Returns the `Span` if there is one. + * @deprecated Use `getActiveSpan()` instead. */ getSpan(): Span | undefined; /** - * Returns the `Transaction` attached to the scope (if there is one) + * Returns the `Transaction` attached to the scope (if there is one). + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. */ getTransaction(): Transaction | undefined; @@ -229,4 +235,32 @@ export interface Scope { * Get propagation context from the scope, used for distributed tracing */ getPropagationContext(): PropagationContext; + + /** + * Capture an exception for this scope. + * + * @param exception The exception to capture. + * @param hint Optinal additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. + */ + captureException(exception: unknown, hint?: EventHint): string; + + /** + * Capture a message for this scope. + * + * @param exception The exception to capture. + * @param level An optional severity level to report the message with. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured message. + */ + captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string; + + /** + * Capture a Sentry event for this scope. + * + * @param exception The event to capture. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured event. + */ + captureEvent(event: Event, hint?: EventHint): string; } diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 4ad0e5aa1110..e85fda90642a 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -28,10 +28,66 @@ export type SpanAttributes = Record; /** This type is aligned with the OpenTelemetry TimeInput type. */ export type SpanTimeInput = HrTime | number | Date; +/** A JSON representation of a span. */ +export interface SpanJSON { + data?: { [key: string]: any }; + description?: string; + op?: string; + parent_span_id?: string; + span_id: string; + start_timestamp: number; + status?: string; + tags?: { [key: string]: Primitive }; + timestamp?: number; + trace_id: string; + origin?: SpanOrigin; +} + +// These are aligned with OpenTelemetry trace flags +type TraceFlagNone = 0x0; +type TraceFlagSampled = 0x1; +export type TraceFlag = TraceFlagNone | TraceFlagSampled; + +export interface SpanContextData { + /** + * The ID of the trace that this span belongs to. It is worldwide unique + * with practically sufficient probability by being made as 16 randomly + * generated bytes, encoded as a 32 lowercase hex characters corresponding to + * 128 bits. + */ + traceId: string; + + /** + * The ID of the Span. It is globally unique with practically sufficient + * probability by being made as 8 randomly generated bytes, encoded as a 16 + * lowercase hex characters corresponding to 64 bits. + */ + spanId: string; + + /** + * Only true if the SpanContext was propagated from a remote parent. + */ + isRemote?: boolean; + + /** + * Trace flags to propagate. + * + * It is represented as 1 byte (bitmap). Bit to represent whether trace is + * sampled or not. When set, the least significant bit documents that the + * caller may have recorded trace data. A caller who does not record trace + * data out-of-band leaves this flag unset. + */ + traceFlags: TraceFlag; + + // Note: we do not have traceState here, but this is optional in OpenTelemetry anyhow +} + /** Interface holding all properties that can be set on a Span on creation. */ export interface SpanContext { /** * Description of the Span. + * + * @deprecated Use `name` instead. */ description?: string; @@ -58,8 +114,6 @@ export interface SpanContext { /** * Was this span chosen to be sent as part of the sample? - * - * @deprecated Use `isRecording()` instead. */ sampled?: boolean; @@ -75,11 +129,13 @@ export interface SpanContext { /** * Tags of the Span. + * @deprecated Pass `attributes` instead. */ tags?: { [key: string]: Primitive }; /** * Data of the Span. + * @deprecated Pass `attributes` instead. */ data?: { [key: string]: any }; @@ -113,52 +169,76 @@ export interface SpanContext { export interface Span extends SpanContext { /** * Human-readable identifier for the span. Identical to span.description. + * @deprecated Use `spanToJSON(span).description` instead. */ name: string; /** - * @inheritDoc + * The ID of the span. + * @deprecated Use `spanContext().spanId` instead. */ spanId: string; /** - * @inheritDoc + * The ID of the trace. + * @deprecated Use `spanContext().traceId` instead. */ traceId: string; + /** + * Was this span chosen to be sent as part of the sample? + * @deprecated Use `isRecording()` instead. + */ + sampled?: boolean; + /** * @inheritDoc */ startTimestamp: number; /** - * @inheritDoc + * Tags for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ tags: { [key: string]: Primitive }; /** - * @inheritDoc + * Data for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ data: { [key: string]: any }; /** - * @inheritDoc + * Attributes for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ attributes: SpanAttributes; /** * The transaction containing this span + * @deprecated Use top level `Sentry.getRootSpan()` instead */ transaction?: Transaction; /** * The instrumenter that created this span. + * + * @deprecated this field will be removed. */ instrumenter: Instrumenter; + /** + * Get context data for this span. + * This includes the spanId & the traceId. + */ + spanContext(): SpanContextData; + /** * Sets the finish timestamp on the current span. + * * @param endTimestamp Takes an endTimestamp if the end should not be the time when you call this function. + * + * @deprecated Use `.end()` instead. */ finish(endTimestamp?: number): void; @@ -174,6 +254,7 @@ export interface Span extends SpanContext { * * @param key Tag key * @param value Tag value + * @deprecated Use `setAttribute()` instead. */ setTag(key: string, value: Primitive): this; @@ -181,6 +262,7 @@ export interface Span extends SpanContext { * Sets the data attribute on the current span * @param key Data key * @param value Data value + * @deprecated Use `setAttribute()` instead. */ setData(key: string, value: any): this; @@ -224,6 +306,8 @@ export interface Span extends SpanContext { /** * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. * Also the `sampled` decision will be inherited. + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ startChild(spanContext?: Pick>): Span; @@ -256,20 +340,11 @@ export interface Span extends SpanContext { */ getTraceContext(): TraceContext; - /** Convert the object to JSON */ - toJSON(): { - data?: { [key: string]: any }; - description?: string; - op?: string; - parent_span_id?: string; - span_id: string; - start_timestamp: number; - status?: string; - tags?: { [key: string]: Primitive }; - timestamp?: number; - trace_id: string; - origin?: SpanOrigin; - }; + /** + * Convert the object to JSON. + * @deprecated Use `spanToJSON(span)` instead. + */ + toJSON(): SpanJSON; /** * If this is span is actually recording data. diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index a4bee40983a5..b297dd0ea9c2 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -30,6 +30,7 @@ export interface TransactionContext extends SpanContext { /** * Metadata associated with the transaction, for internal SDK use. + * @deprecated Use attributes or store data on the scope instead. */ metadata?: Partial; } @@ -44,52 +45,75 @@ export type TraceparentData = Pick { /** - * @inheritDoc + * Human-readable identifier for the transaction. + * @deprecated Use `spanToJSON(span).description` instead. + */ + name: string; + + /** + * The ID of the transaction. + * @deprecated Use `spanContext().spanId` instead. */ spanId: string; /** - * @inheritDoc + * The ID of the trace. + * @deprecated Use `spanContext().traceId` instead. */ traceId: string; + /** + * Was this transaction chosen to be sent as part of the sample? + * @deprecated Use `spanIsSampled(transaction)` instead. + */ + sampled?: boolean; + /** * @inheritDoc */ startTimestamp: number; /** - * @inheritDoc + * Tags for the transaction. + * @deprecated Use `getSpanAttributes(transaction)` instead. */ tags: { [key: string]: Primitive }; /** - * @inheritDoc + * Data for the transaction. + * @deprecated Use `getSpanAttributes(transaction)` instead. */ data: { [key: string]: any }; /** - * @inheritDoc + * Attributes for the transaction. + * @deprecated Use `getSpanAttributes(transaction)` instead. */ attributes: SpanAttributes; /** - * Metadata about the transaction + * Metadata about the transaction. + * @deprecated Use attributes or store data on the scope instead. */ metadata: TransactionMetadata; /** * The instrumenter that created this transaction. + * + * @deprecated This field will be removed in v8. */ instrumenter: Instrumenter; /** * Set the name of the transaction + * + * @deprecated Use `.updateName()` and `.setAttribute()` instead. */ setName(name: string, source?: TransactionMetadata['source']): void; /** - * Set the context of a transaction event + * Set the context of a transaction event. + * @deprecated Use either `.setAttribute()`, or set the context on the scope before creating the transaction. */ setContext(key: string, context: Context): void; @@ -99,6 +123,8 @@ export interface Transaction extends TransactionContext, Omit): void; - /** Return the current Dynamic Sampling Context of this transaction */ + /** + * Return the current Dynamic Sampling Context of this transaction + * + * @deprecated Use top-level `getDynamicSamplingContextFromSpan` instead. + */ getDynamicSamplingContext(): Partial; } @@ -160,7 +190,10 @@ export interface SamplingContext extends CustomSamplingContext { } export interface TransactionMetadata { - /** The sample rate used when sampling this transaction */ + /** + * The sample rate used when sampling this transaction. + * @deprecated Use `SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE` attribute instead. + */ sampleRate?: number; /** @@ -182,10 +215,16 @@ export interface TransactionMetadata { /** TODO: If we rm -rf `instrumentServer`, this can go, too */ requestPath?: string; - /** Information on how a transaction name was generated. */ + /** + * Information on how a transaction name was generated. + * @deprecated Use `SEMANTIC_ATTRIBUTE_SENTRY_SOURCE` attribute instead. + */ source: TransactionSource; - /** Metadata for the transaction's spans, keyed by spanId */ + /** + * Metadata for the transaction's spans, keyed by spanId. + * @deprecated This will be removed in v8. + */ spanMetadata: { [spanId: string]: { [key: string]: unknown } }; } diff --git a/packages/typescript/package.json b/packages/typescript/package.json index fa20f12b7936..a96d4e4f0e40 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "7.92.0", + "version": "7.93.0", "description": "Typescript configuration used at Sentry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript", diff --git a/packages/utils/package.json b/packages/utils/package.json index b142ce702d06..da47f9d7ad6f 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/utils", - "version": "7.92.0", + "version": "7.93.0", "description": "Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/utils", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@sentry/types": "7.92.0" + "@sentry/types": "7.93.0" }, "devDependencies": { "@types/array.prototype.flat": "^1.2.1", diff --git a/packages/utils/src/eventbuilder.ts b/packages/utils/src/eventbuilder.ts index 2a65a6d014cf..74b5aff6538e 100644 --- a/packages/utils/src/eventbuilder.ts +++ b/packages/utils/src/eventbuilder.ts @@ -74,7 +74,11 @@ export function eventFromUnknownInput( exception: unknown, hint?: EventHint, ): Event { - const client = typeof getHubOrClient === 'function' ? getHubOrClient().getClient() : getHubOrClient; + const client = + typeof getHubOrClient === 'function' + ? // eslint-disable-next-line deprecation/deprecation + getHubOrClient().getClient() + : getHubOrClient; let ex: unknown = exception; const providedMechanism: Mechanism | undefined = diff --git a/packages/utils/src/node-stack-trace.ts b/packages/utils/src/node-stack-trace.ts index 43db209a5fc5..3b486be9148c 100644 --- a/packages/utils/src/node-stack-trace.ts +++ b/packages/utils/src/node-stack-trace.ts @@ -35,7 +35,7 @@ export function filenameIsInApp(filename: string, isNative: boolean = false): bo // It's not internal if it's an absolute linux path !filename.startsWith('/') && // It's not internal if it's an absolute windows path - !filename.includes(':\\') && + !filename.match(/^[A-Z]:/) && // It's not internal if the path is starting with a dot !filename.startsWith('.') && // It's not internal if the frame has a protocol. In node, this is usually the case if the file got pre-processed with a bundler like webpack @@ -103,6 +103,11 @@ export function node(getModule?: GetModuleFn): StackLineParserFn { let filename = lineMatch[2] && lineMatch[2].startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2]; const isNative = lineMatch[5] === 'native'; + // If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo` + if (filename && filename.match(/\/[A-Z]:/)) { + filename = filename.slice(1); + } + if (!filename && lineMatch[5] && !isNative) { filename = lineMatch[5]; } diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index 0249b7a2b481..aaa1898e1f55 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -72,16 +72,21 @@ export function addRequestDataToTransaction( deps?: InjectedNodeDeps, ): void { if (!transaction) return; + // eslint-disable-next-line deprecation/deprecation if (!transaction.metadata.source || transaction.metadata.source === 'url') { // Attempt to grab a parameterized route off of the request const [name, source] = extractPathForTransaction(req, { path: true, method: true }); transaction.updateName(name); + // TODO: SEMANTIC_ATTRIBUTE_SENTRY_SOURCE is in core, align this once we merge utils & core + // eslint-disable-next-line deprecation/deprecation transaction.setMetadata({ source }); } - transaction.setData('url', req.originalUrl || req.url); + transaction.setAttribute('url', req.originalUrl || req.url); if (req.baseUrl) { - transaction.setData('baseUrl', req.baseUrl); + transaction.setAttribute('baseUrl', req.baseUrl); } + // TODO: We need to rewrite this to a flat format? + // eslint-disable-next-line deprecation/deprecation transaction.setData('query', extractQueryParams(req, deps)); } @@ -187,6 +192,7 @@ export function extractRequestData( }, ): ExtractedNodeRequestData { const { include = DEFAULT_REQUEST_INCLUDES, deps } = options || {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const requestData: { [key: string]: any } = {}; // headers: diff --git a/packages/utils/src/time.ts b/packages/utils/src/time.ts index 1c366f09d952..16c7058ae68c 100644 --- a/packages/utils/src/time.ts +++ b/packages/utils/src/time.ts @@ -1,26 +1,6 @@ -import { dynamicRequire, isNodeEnv } from './node'; -import { getGlobalObject } from './worldwide'; +import { GLOBAL_OBJ } from './worldwide'; -// eslint-disable-next-line deprecation/deprecation -const WINDOW = getGlobalObject(); - -/** - * An object that can return the current timestamp in seconds since the UNIX epoch. - */ -interface TimestampSource { - nowSeconds(): number; -} - -/** - * A TimestampSource implementation for environments that do not support the Performance Web API natively. - * - * Note that this TimestampSource does not use a monotonic clock. A call to `nowSeconds` may return a timestamp earlier - * than a previously returned value. We do not try to emulate a monotonic behavior in order to facilitate debugging. It - * is more obvious to explain "why does my span have negative duration" than "why my spans have zero duration". - */ -const dateTimestampSource: TimestampSource = { - nowSeconds: () => Date.now() / 1000, -}; +const ONE_SECOND_IN_MS = 1000; /** * A partial definition of the [Performance Web API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Performance} @@ -37,89 +17,56 @@ interface Performance { now(): number; } +/** + * Returns a timestamp in seconds since the UNIX epoch using the Date API. + * + * TODO(v8): Return type should be rounded. + */ +export function dateTimestampInSeconds(): number { + return Date.now() / ONE_SECOND_IN_MS; +} + /** * Returns a wrapper around the native Performance API browser implementation, or undefined for browsers that do not * support the API. * * Wrapping the native API works around differences in behavior from different browsers. */ -function getBrowserPerformance(): Performance | undefined { - const { performance } = WINDOW; +function createUnixTimestampInSecondsFunc(): () => number { + const { performance } = GLOBAL_OBJ as typeof GLOBAL_OBJ & { performance?: Performance }; if (!performance || !performance.now) { - return undefined; + return dateTimestampInSeconds; } - // Replace performance.timeOrigin with our own timeOrigin based on Date.now(). - // - // This is a partial workaround for browsers reporting performance.timeOrigin such that performance.timeOrigin + - // performance.now() gives a date arbitrarily in the past. - // - // Additionally, computing timeOrigin in this way fills the gap for browsers where performance.timeOrigin is - // undefined. - // - // The assumption that performance.timeOrigin + performance.now() ~= Date.now() is flawed, but we depend on it to - // interact with data coming out of performance entries. - // - // Note that despite recommendations against it in the spec, browsers implement the Performance API with a clock that - // might stop when the computer is asleep (and perhaps under other circumstances). Such behavior causes - // performance.timeOrigin + performance.now() to have an arbitrary skew over Date.now(). In laptop computers, we have - // observed skews that can be as long as days, weeks or months. - // - // See https://github.com/getsentry/sentry-javascript/issues/2590. + // Some browser and environments don't have a timeOrigin, so we fallback to + // using Date.now() to compute the starting time. + const approxStartingTimeOrigin = Date.now() - performance.now(); + const timeOrigin = performance.timeOrigin == undefined ? approxStartingTimeOrigin : performance.timeOrigin; + + // performance.now() is a monotonic clock, which means it starts at 0 when the process begins. To get the current + // wall clock time (actual UNIX timestamp), we need to add the starting time origin and the current time elapsed. // - // BUG: despite our best intentions, this workaround has its limitations. It mostly addresses timings of pageload - // transactions, but ignores the skew built up over time that can aversely affect timestamps of navigation - // transactions of long-lived web pages. - const timeOrigin = Date.now() - performance.now(); - - return { - now: () => performance.now(), - timeOrigin, + // TODO: This does not account for the case where the monotonic clock that powers performance.now() drifts from the + // wall clock time, which causes the returned timestamp to be inaccurate. We should investigate how to detect and + // correct for this. + // See: https://github.com/getsentry/sentry-javascript/issues/2590 + // See: https://github.com/mdn/content/issues/4713 + // See: https://dev.to/noamr/when-a-millisecond-is-not-a-millisecond-3h6 + return () => { + return (timeOrigin + performance.now()) / ONE_SECOND_IN_MS; }; } -/** - * Returns the native Performance API implementation from Node.js. Returns undefined in old Node.js versions that don't - * implement the API. - */ -function getNodePerformance(): Performance | undefined { - try { - const perfHooks = dynamicRequire(module, 'perf_hooks') as { performance: Performance }; - return perfHooks.performance; - } catch (_) { - return undefined; - } -} - -/** - * The Performance API implementation for the current platform, if available. - */ -const platformPerformance: Performance | undefined = isNodeEnv() ? getNodePerformance() : getBrowserPerformance(); - -const timestampSource: TimestampSource = - platformPerformance === undefined - ? dateTimestampSource - : { - nowSeconds: () => (platformPerformance.timeOrigin + platformPerformance.now()) / 1000, - }; - -/** - * Returns a timestamp in seconds since the UNIX epoch using the Date API. - */ -export const dateTimestampInSeconds: () => number = dateTimestampSource.nowSeconds.bind(dateTimestampSource); - /** * Returns a timestamp in seconds since the UNIX epoch using either the Performance or Date APIs, depending on the * availability of the Performance API. * - * See `usingPerformanceAPI` to test whether the Performance API is used. - * * BUG: Note that because of how browsers implement the Performance API, the clock might stop when the computer is * asleep. This creates a skew between `dateTimestampInSeconds` and `timestampInSeconds`. The * skew can grow to arbitrary amounts like days, weeks or months. * See https://github.com/getsentry/sentry-javascript/issues/2590. */ -export const timestampInSeconds: () => number = timestampSource.nowSeconds.bind(timestampSource); +export const timestampInSeconds = createUnixTimestampInSecondsFunc(); /** * Re-exported with an old name for backwards-compatibility. @@ -129,11 +76,6 @@ export const timestampInSeconds: () => number = timestampSource.nowSeconds.bind( */ export const timestampWithMs = timestampInSeconds; -/** - * A boolean that is true when timestampInSeconds uses the Performance API to produce monotonic timestamps. - */ -export const usingPerformanceAPI = platformPerformance !== undefined; - /** * Internal helper to store what is the source of browserPerformanceTimeOrigin below. For debugging only. */ @@ -148,7 +90,7 @@ export const browserPerformanceTimeOrigin = ((): number | undefined => { // performance.timing.navigationStart, which results in poor results in performance data. We only treat time origin // data as reliable if they are within a reasonable threshold of the current time. - const { performance } = WINDOW; + const { performance } = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; if (!performance || !performance.now) { _browserPerformanceTimeOriginMode = 'none'; return undefined; diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index a3e2ad452a55..8ad43740fb0b 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vercel-edge", - "version": "7.92.0", + "version": "7.93.0", "description": "Offical Sentry SDK for the Vercel Edge Runtime", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vercel-edge", @@ -29,10 +29,10 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "7.92.0", - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry-internal/tracing": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "devDependencies": { "@edge-runtime/jest-environment": "2.2.3", diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index ff8d97fbf398..8fcc124256dc 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -38,6 +38,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -64,6 +65,7 @@ export { // eslint-disable-next-line deprecation/deprecation trace, withScope, + withIsolationScope, captureCheckIn, withMonitor, setMeasurement, diff --git a/packages/vercel-edge/src/integrations/wintercg-fetch.ts b/packages/vercel-edge/src/integrations/wintercg-fetch.ts index 18c4cb25df56..5d3d662e5b4f 100644 --- a/packages/vercel-edge/src/integrations/wintercg-fetch.ts +++ b/packages/vercel-edge/src/integrations/wintercg-fetch.ts @@ -1,5 +1,5 @@ import { instrumentFetchRequest } from '@sentry-internal/tracing'; -import { addBreadcrumb, getClient, getCurrentHub, isSentryRequestUrl } from '@sentry/core'; +import { addBreadcrumb, getClient, isSentryRequestUrl } from '@sentry/core'; import type { FetchBreadcrumbData, FetchBreadcrumbHint, HandlerDataFetch, Integration, Span } from '@sentry/types'; import { LRUMap, addFetchInstrumentationHandler, stringMatchesSomePattern } from '@sentry/utils'; @@ -49,8 +49,7 @@ export class WinterCGFetch implements Integration { const spans: Record = {}; addFetchInstrumentationHandler(handlerData => { - const hub = getCurrentHub(); - if (!hub.getIntegration(WinterCGFetch)) { + if (!getClient()?.getIntegrationByName?.('WinterCGFetch')) { return; } diff --git a/packages/vercel-edge/test/wintercg-fetch.test.ts b/packages/vercel-edge/test/wintercg-fetch.test.ts index 3e6ac09330e0..e690e6785c79 100644 --- a/packages/vercel-edge/test/wintercg-fetch.test.ts +++ b/packages/vercel-edge/test/wintercg-fetch.test.ts @@ -1,20 +1,26 @@ import * as internalTracing from '@sentry-internal/tracing'; import * as sentryCore from '@sentry/core'; -import type { HandlerDataFetch, Integration, IntegrationClass } from '@sentry/types'; +import type { HandlerDataFetch, Integration } from '@sentry/types'; import * as sentryUtils from '@sentry/utils'; import { createStackParser } from '@sentry/utils'; import { VercelEdgeClient } from '../src/index'; import { WinterCGFetch } from '../src/integrations/wintercg-fetch'; -class FakeHub extends sentryCore.Hub { - getIntegration(integration: IntegrationClass): T | null { - return new integration(); +class FakeClient extends VercelEdgeClient { + public getIntegrationByName(name: string): T | undefined { + return name === 'WinterCGFetch' ? (new WinterCGFetch() as unknown as T) : undefined; } } -const fakeHubInstance = new FakeHub( - new VercelEdgeClient({ +const addFetchInstrumentationHandlerSpy = jest.spyOn(sentryUtils, 'addFetchInstrumentationHandler'); +const instrumentFetchRequestSpy = jest.spyOn(internalTracing, 'instrumentFetchRequest'); +const addBreadcrumbSpy = jest.spyOn(sentryCore, 'addBreadcrumb'); + +beforeEach(() => { + jest.clearAllMocks(); + + const client = new FakeClient({ dsn: 'https://public@dsn.ingest.sentry.io/1337', enableTracing: true, tracesSampleRate: 1, @@ -25,19 +31,9 @@ const fakeHubInstance = new FakeHub( }), tracePropagationTargets: ['http://my-website.com/'], stackParser: createStackParser(), - }), -); - -jest.spyOn(sentryCore, 'getCurrentHub').mockImplementation(() => fakeHubInstance); -jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => fakeHubInstance.getScope()); -jest.spyOn(sentryCore, 'getClient').mockImplementation(() => fakeHubInstance.getClient()); - -const addFetchInstrumentationHandlerSpy = jest.spyOn(sentryUtils, 'addFetchInstrumentationHandler'); -const instrumentFetchRequestSpy = jest.spyOn(internalTracing, 'instrumentFetchRequest'); -const addBreadcrumbSpy = jest.spyOn(sentryCore, 'addBreadcrumb'); + }); -beforeEach(() => { - jest.clearAllMocks(); + jest.spyOn(sentryCore, 'getClient').mockImplementation(() => client); }); describe('WinterCGFetch instrumentation', () => { diff --git a/packages/vue/package.json b/packages/vue/package.json index b13e68db9e37..ad020b4f8f1d 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vue", - "version": "7.92.0", + "version": "7.93.0", "description": "Official Sentry SDK for Vue.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue", @@ -29,10 +29,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.92.0", - "@sentry/core": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/browser": "7.93.0", + "@sentry/core": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "peerDependencies": { "vue": "2.x || 3.x" diff --git a/packages/vue/src/errorhandler.ts b/packages/vue/src/errorhandler.ts index 9d09bdf8c181..725f9b56c714 100644 --- a/packages/vue/src/errorhandler.ts +++ b/packages/vue/src/errorhandler.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/browser'; +import { captureException } from '@sentry/core'; import { consoleSandbox } from '@sentry/utils'; import type { ViewModel, Vue, VueOptions } from './types'; @@ -30,7 +30,7 @@ export const attachErrorHandler = (app: Vue, options: VueOptions): void => { // Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time. setTimeout(() => { - getCurrentHub().captureException(error, { + captureException(error, { captureContext: { contexts: { vue: metadata } }, mechanism: { handled: false }, }); diff --git a/packages/vue/src/integration.ts b/packages/vue/src/integration.ts index e34c3493d1e0..1d3bfa47cc19 100644 --- a/packages/vue/src/integration.ts +++ b/packages/vue/src/integration.ts @@ -46,6 +46,7 @@ export class VueIntegration implements Integration { /** Just here for easier testing */ protected _setupIntegration(hub: Hub): void { + // eslint-disable-next-line deprecation/deprecation const client = hub.getClient(); const options: Options = { ...DEFAULT_CONFIG, ...(client && client.getOptions()), ...this._options }; diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 70117e960fe2..98c18ae80691 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -1,4 +1,5 @@ import { WINDOW, captureException } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getActiveTransaction } from './tracing'; @@ -72,8 +73,8 @@ export function vueRouterInstrumentation( op: 'pageload', origin: 'auto.pageload.vue', tags, - metadata: { - source: 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }); } @@ -91,7 +92,7 @@ export function vueRouterInstrumentation( // hence only '==' instead of '===', because `undefined == null` evaluates to `true` const isPageLoadNavigation = from.name == null && from.matched.length === 0; - const data = { + const data: Record = { params: to.params, query: to.query, }; @@ -108,27 +109,30 @@ export function vueRouterInstrumentation( } if (startTransactionOnPageLoad && isPageLoadNavigation) { + // eslint-disable-next-line deprecation/deprecation const pageloadTransaction = getActiveTransaction(); if (pageloadTransaction) { - if (pageloadTransaction.metadata.source !== 'custom') { + const attributes = spanToJSON(pageloadTransaction).data || {}; + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') { pageloadTransaction.updateName(transactionName); - pageloadTransaction.setMetadata({ source: transactionSource }); + pageloadTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); } + // TODO: We need to flatten these to make them attributes + // eslint-disable-next-line deprecation/deprecation pageloadTransaction.setData('params', data.params); + // eslint-disable-next-line deprecation/deprecation pageloadTransaction.setData('query', data.query); } } if (startTransactionOnLocationChange && !isPageLoadNavigation) { + data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource; startTransaction({ name: transactionName, op: 'navigation', origin: 'auto.navigation.vue', tags, data, - metadata: { - source: transactionSource, - }, }); } diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index af93701e74a7..cff758c4b571 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -32,8 +32,13 @@ const HOOKS: { [key in Operation]: Hook[] } = { update: ['beforeUpdate', 'updated'], }; -/** Grabs active transaction off scope, if any */ +/** + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. + */ export function getActiveTransaction(): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getTransaction(); } @@ -73,10 +78,12 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { const isRoot = this.$root === this; if (isRoot) { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { this.$_sentryRootSpan = this.$_sentryRootSpan || + // eslint-disable-next-line deprecation/deprecation activeTransaction.startChild({ description: 'Application Render', op: `${VUE_OP}.render`, @@ -101,6 +108,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { // Start a new span if current hook is a 'before' hook. // Otherwise, retrieve the current span and finish it. if (internalHook == internalHooks[0]) { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = (this.$root && this.$root.$_sentryRootSpan) || getActiveTransaction(); if (activeTransaction) { // Cancel old span for this hook operation in case it didn't get cleaned up. We're not actually sure if it @@ -111,6 +119,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { oldSpan.end(); } + // eslint-disable-next-line deprecation/deprecation this.$_sentrySpans[operation] = activeTransaction.startChild({ description: `Vue <${name}>`, op: `${VUE_OP}.${operation}`, diff --git a/packages/vue/test/integration/init.test.ts b/packages/vue/test/integration/init.test.ts index e176e5b1691c..ed69034ebbf7 100644 --- a/packages/vue/test/integration/init.test.ts +++ b/packages/vue/test/integration/init.test.ts @@ -104,7 +104,7 @@ Update your \`Sentry.init\` call with an appropriate config option: }); function runInit(options: Partial): void { - const hasRunBefore = Sentry.getCurrentHub().getIntegration(VueIntegration); + const hasRunBefore = Sentry.getClient()?.getIntegrationByName?.(VueIntegration.id); const integration = new VueIntegration(); diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index 2e937e02f154..061bcdd3e1f9 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -1,4 +1,5 @@ import * as SentryBrowser from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { vueRouterInstrumentation } from '../src'; @@ -100,10 +101,8 @@ describe('vueRouterInstrumentation()', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenCalledWith({ name: transactionName, - metadata: { - source: transactionSource, - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: transactionSource, params: to.params, query: to.query, }, @@ -128,7 +127,7 @@ describe('vueRouterInstrumentation()', () => { const mockedTxn = { updateName: jest.fn(), setData: jest.fn(), - setMetadata: jest.fn(), + setAttribute: jest.fn(), metadata: {}, }; const customMockStartTxn = { ...mockStartTransaction }.mockImplementation(_ => { @@ -146,8 +145,8 @@ describe('vueRouterInstrumentation()', () => { expect(customMockStartTxn).toHaveBeenCalledTimes(1); expect(customMockStartTxn).toHaveBeenCalledWith({ name: '/', - metadata: { - source: 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, op: 'pageload', origin: 'auto.pageload.vue', @@ -165,7 +164,7 @@ describe('vueRouterInstrumentation()', () => { expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); expect(mockedTxn.updateName).toHaveBeenCalledWith(transactionName); - expect(mockedTxn.setMetadata).toHaveBeenCalledWith({ source: transactionSource }); + expect(mockedTxn.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); expect(mockedTxn.setData).toHaveBeenNthCalledWith(1, 'params', to.params); expect(mockedTxn.setData).toHaveBeenNthCalledWith(2, 'query', to.query); @@ -190,10 +189,8 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/login', - metadata: { - source: 'route', - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', params: to.params, query: to.query, }, @@ -222,10 +219,8 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: 'login-screen', - metadata: { - source: 'custom', - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', params: to.params, query: to.query, }, @@ -241,10 +236,13 @@ describe('vueRouterInstrumentation()', () => { const mockedTxn = { updateName: jest.fn(), setData: jest.fn(), + setAttribute: jest.fn(), name: '', - metadata: { - source: 'url', - }, + toJSON: () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }), }; const customMockStartTxn = { ...mockStartTransaction }.mockImplementation(_ => { return mockedTxn; @@ -261,8 +259,8 @@ describe('vueRouterInstrumentation()', () => { expect(customMockStartTxn).toHaveBeenCalledTimes(1); expect(customMockStartTxn).toHaveBeenCalledWith({ name: '/', - metadata: { - source: 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, op: 'pageload', origin: 'auto.pageload.vue', @@ -274,7 +272,11 @@ describe('vueRouterInstrumentation()', () => { // now we give the transaction a custom name, thereby simulating what would // happen when users use the `beforeNavigate` hook mockedTxn.name = 'customTxnName'; - mockedTxn.metadata.source = 'custom'; + mockedTxn.toJSON = () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + }, + }); const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; beforeEachCallback(testRoutes['normalRoute1'], testRoutes['initialPageloadRoute'], mockNext); @@ -282,7 +284,7 @@ describe('vueRouterInstrumentation()', () => { expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); expect(mockedTxn.updateName).not.toHaveBeenCalled(); - expect(mockedTxn.metadata.source).toEqual('custom'); + expect(mockedTxn.setAttribute).not.toHaveBeenCalled(); expect(mockedTxn.name).toEqual('customTxnName'); }); @@ -344,10 +346,8 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/login', - metadata: { - source: 'route', - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', params: to.params, query: to.query, }, diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 4e01575a88f7..5f26d365be96 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/wasm", - "version": "7.92.0", + "version": "7.93.0", "description": "Support for WASM.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm", @@ -29,9 +29,9 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.92.0", - "@sentry/types": "7.92.0", - "@sentry/utils": "7.92.0" + "@sentry/browser": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0" }, "scripts": { "build": "run-p build:transpile build:bundle build:types", diff --git a/yarn.lock b/yarn.lock index 48857bd5a97d..a277de76c4f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5231,33 +5231,33 @@ semver "7.3.2" semver-intersect "1.4.0" -"@sentry-internal/rrdom@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.6.0.tgz#19b5ab7a01ad5031be2d4bcedd4afedb44ee2bed" - integrity sha512-XqxOhLk/CdrKh0toOKeQ6mOcjLDK3B1KY/UVqM9VwhdVhiHeMwPj6GjJUoNkEXh0MwkDM0pzIMv95oSq7hGhPg== +"@sentry-internal/rrdom@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.7.3.tgz#2efe68a9cf23de9a8970acf4303748cdd7866b20" + integrity sha512-XD14G4Lv3ppvJlR7VkkCgHTKu1ylh7yvXdSsN5/FyGTH+IAXQIKL5nINIgWZTN3noNBWV9R0vcHDufXG/WktWA== dependencies: - "@sentry-internal/rrweb-snapshot" "2.6.0" + "@sentry-internal/rrweb-snapshot" "2.7.3" -"@sentry-internal/rrweb-snapshot@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.6.0.tgz#1b214c9ab4645138a02ef255c95d811bd852f996" - integrity sha512-dlduO37avs5HBP8zRxFHlhRb7ZP6p3SrgMSztPCCnfYr/XAB/rn5yeVn9U2FDYdrgyUzPjFWfYWFvm1eJuEMSg== +"@sentry-internal/rrweb-snapshot@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.7.3.tgz#9a7173825a31c07ccf27a5956f154400e11fdd97" + integrity sha512-mSZuBPmWia3x9wCuaJiZMD9ZVDnFv7TSG1Nz9X4ZqWb3DdaxB2MogGUU/2aTVqmRj6F91nl+GHb5NpmNYojUsw== -"@sentry-internal/rrweb-types@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.6.0.tgz#e526994125db6684ce9402d96f64318f062bebb0" - integrity sha512-mPPumdbyNHF24zShvZqzqgkZRsJHhlNpglGTS0cR/PkX2QdG0CtsPVFpaYj6UQAFGpfb2Aj7VdkKuuzX4RX69w== +"@sentry-internal/rrweb-types@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.7.3.tgz#e38737bc5c31aa9dfb8ce8faf46af374c2aa7cbb" + integrity sha512-EqALxhZtvH0rimYfj7J48DRC+fj+AGsZ/VDdOPKh3MQXptTyHncoWBj4ZtB1AaH7foYUr+2wkyxl3HqMVwe+6g== dependencies: - "@sentry-internal/rrweb-snapshot" "2.6.0" + "@sentry-internal/rrweb-snapshot" "2.7.3" -"@sentry-internal/rrweb@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.6.0.tgz#0976e0d021c965b491a5193546a735f78dad9107" - integrity sha512-N+v0cgft/mikwIH5MPIspWNEqHa3E/01rA+IwozTs/TUp2e8sJmF3qxR0+OeBGZU0ln4soG1o18FjsCmadqbeQ== +"@sentry-internal/rrweb@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.7.3.tgz#900ce7b1bd8ec43b5557d73698cc1b4dc9f47f72" + integrity sha512-K2pksQ1FgDumv54r1o0+RAKB3Qx3Zgx6OrQAkU/SI6/7HcBIxe7b4zepg80NyvnfohUs/relw2EoD3K3kqd8tg== dependencies: - "@sentry-internal/rrdom" "2.6.0" - "@sentry-internal/rrweb-snapshot" "2.6.0" - "@sentry-internal/rrweb-types" "2.6.0" + "@sentry-internal/rrdom" "2.7.3" + "@sentry-internal/rrweb-snapshot" "2.7.3" + "@sentry-internal/rrweb-types" "2.7.3" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" @@ -5292,40 +5292,40 @@ magic-string "0.27.0" unplugin "1.0.1" -"@sentry/cli-darwin@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.23.2.tgz#d1fed31063e19bfbdf5d5ab0bb9938f407eb9e33" - integrity sha512-7Jw1yEmJxiNan5WJyiAKXascxoe8uccKVaTvEo0JwzgWhPzS71j3eUlthuQuy0xv5Pqw4d89khAP79X/pzW/dw== - -"@sentry/cli-linux-arm64@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.23.2.tgz#a4171da7de22fd31a359fdd5671b9e445316778c" - integrity sha512-Hs2PbK2++r6Lbss44HIDXJwBSIyw1naLdIpOBi9NVLBGZxO2VLt8sQYDhVDv2ZIUijw1aGc5sg8R7R0/6qqr8Q== - -"@sentry/cli-linux-arm@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.23.2.tgz#3a101ffcc37128eeebd9abdbe033cf9fcbf093ad" - integrity sha512-fQZNHsGO6kRPT7nuv/GZ048rA2aEGHcrTZEN6UhgHoowPGGmfSpOqlpdXLME6WYWzWeSBt5Sy5RcxMvPzuDnRQ== - -"@sentry/cli-linux-i686@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.23.2.tgz#fcbaa045aa8ab2d646d6c27274f88ccc29bb417c" - integrity sha512-emogfai7xCySsTAaixjnh0hgzcb2nhEqz7MRYxGA+rSI8IgP1ZMBfdWHA/4fUap0wLNA6vVgvbHlFcBVQcGchA== - -"@sentry/cli-linux-x64@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.23.2.tgz#30404f32d8f32e33a24fed67f11f11ac84da0a6c" - integrity sha512-VewJmJRUFvKR3YiPp1pZOZJxrFGLgBHLGEP/9wBkkp3cY+rKrzQ3b7Dlh9v+YOkz1qjF1R1FsAzvsYd9/05dLg== - -"@sentry/cli-win32-i686@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.23.2.tgz#6f77749aad856dceaa9b206c6bd51fbc8caca704" - integrity sha512-R8olErQICIV+AdjINxLQYKVGRi49PdSykjs94gfTvJBxb2hvqCpS+LIVS5SFu2UDvT3/9Elq6hXMKxEgYNy0pQ== - -"@sentry/cli-win32-x64@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.23.2.tgz#d68ac046568ca951d5bfe7216ae5c52a07a65ecc" - integrity sha512-GK9xburDBnpBmjtbWrMK+9I7DRKbEhmjfWLdoTQK593xOHPOzy8lhDZ1u9Lp1mUKUcG1xba4BOFZgNppMYG2cA== +"@sentry/cli-darwin@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.24.1.tgz#feae406b2bf9a6a736e5a6f31e5561aaae8ed902" + integrity sha512-L6puTcZn5AarTL9YCVCSSCJoMV7opMx5hwwl0+sQGbLO8BChuC2QZl+j4ftEb3WgnFcT5+OODBlu4ocREtG7sQ== + +"@sentry/cli-linux-arm64@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.24.1.tgz#a3e5339904fcb89167736a4a3c6edcd697972c24" + integrity sha512-47fq/sOZnY8oSnuEurlplHKlcEhCf4Pd3JHmV6N8dYYwPEapoELb3V53BDPhjkj/rwdpJf8T90+LXCQkeF/o+w== + +"@sentry/cli-linux-arm@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.24.1.tgz#3784604555de7baa17b0b7ae8b7912d37d52738e" + integrity sha512-wnOeIl0NzUdSvz7kJKTovksDUwx6TTyV1iBjM19gZGqi+hfNc1bUa1IDGSY0m/T+CpSZiuKL0M36sYet5euDUQ== + +"@sentry/cli-linux-i686@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.24.1.tgz#9ba0c0eaa783bd955060ea105278c2856c42755f" + integrity sha512-OjpP1aRV0cwdtcics0hv8tZR6Bl5+1KIc+0habMeMxfTN7FvGmJb3ZTpuhi8OJLDglppQe6KlWzEFi8UgcE42Q== + +"@sentry/cli-linux-x64@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.24.1.tgz#b7d157b66f76a5ac47a43b934eadeec50e9c5640" + integrity sha512-GfryILChjrgSGBrT90ln46qt6UTI1ebevcDPoWArftTQ0n+P4tPFcfA9bCMV16Jsnc59CtjMFlQknLOAWnezgg== + +"@sentry/cli-win32-i686@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.24.1.tgz#b01158d6e633a2e6dfed59feaeb6979a32d7632c" + integrity sha512-COi7b/g3BbJHlJfF7GA0LAw/foyP3rMfDLQid/4fj7a0DqNjwAJRgazXvvtAY7/3XThHVE/sgLH0UmAgYaBBpA== + +"@sentry/cli-win32-x64@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.24.1.tgz#8b8752606939d229207b1afe6d4a254d24acb47c" + integrity sha512-gJxQw9ppRgZecMZ4t7mi5zWTFssVFbO2V35Nq6qk9bwHACa/LjQbyRqSqOg6zil8QruGvH1oQYClFZHlW8EHuA== "@sentry/cli@^1.74.4", "@sentry/cli@^1.77.1": version "1.77.1" @@ -5340,9 +5340,9 @@ which "^2.0.2" "@sentry/cli@^2.17.0", "@sentry/cli@^2.21.2", "@sentry/cli@^2.23.0": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.23.2.tgz#5b8edd4e6e8fdea05f5d6bb6c84b55d52897c250" - integrity sha512-coQoJnts6E/yN21uQyI7sqa89kixXQuIRodOPnIymQtYJZG3DAwqxcCBLMS3NZyVQ3HemeuhhDnE/KFd1mS53Q== + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.24.1.tgz#9b643ee44c7b2be7cf9b9435b7eea4b92bdfd6cd" + integrity sha512-eXqbKRzychtG8mMfGmqc0DRY677ngHRYa3aVS8f0VVKHK4PPV/ta08ORs0iS73IaasP563r8YEzpYjD74GtSZA== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -5350,13 +5350,13 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.23.2" - "@sentry/cli-linux-arm" "2.23.2" - "@sentry/cli-linux-arm64" "2.23.2" - "@sentry/cli-linux-i686" "2.23.2" - "@sentry/cli-linux-x64" "2.23.2" - "@sentry/cli-win32-i686" "2.23.2" - "@sentry/cli-win32-x64" "2.23.2" + "@sentry/cli-darwin" "2.24.1" + "@sentry/cli-linux-arm" "2.24.1" + "@sentry/cli-linux-arm64" "2.24.1" + "@sentry/cli-linux-i686" "2.24.1" + "@sentry/cli-linux-x64" "2.24.1" + "@sentry/cli-win32-i686" "2.24.1" + "@sentry/cli-win32-x64" "2.24.1" "@sentry/vite-plugin@^0.6.1": version "0.6.1" @@ -6309,11 +6309,6 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== -"@types/mocha@^5.2.0": - version "5.2.7" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" - integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== - "@types/mongodb@^3.6.20": version "3.6.20" resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.20.tgz#b7c5c580644f6364002b649af1c06c3c0454e1d2"