diff --git a/.craft.yml b/.craft.yml index 522b6f125f00..85959ea2e984 100644 --- a/.craft.yml +++ b/.craft.yml @@ -89,8 +89,11 @@ targets: ## 5. Node-based Packages - name: npm - id: '@sentry/serverless' - includeNames: /^sentry-serverless-\d.*\.tgz$/ + id: '@sentry/aws-serverless' + includeNames: /^sentry-aws-serverless-\d.*\.tgz$/ + - name: npm + id: '@sentry/google-cloud-serverless' + includeNames: /^sentry-google-cloud-\d.*\.tgz$/ - name: npm id: '@sentry/bun' includeNames: /^sentry-bun-\d.*\.tgz$/ diff --git a/.eslintrc.js b/.eslintrc.js index 150c7e8efd2f..90f474319c7d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { root: true, env: { - es6: true, + es2017: true, }, parserOptions: { ecmaVersion: 2018, diff --git a/.github/CANARY_FAILURE_TEMPLATE.md b/.github/CANARY_FAILURE_TEMPLATE.md index a99ec1e844f7..9e05fcfc44ae 100644 --- a/.github/CANARY_FAILURE_TEMPLATE.md +++ b/.github/CANARY_FAILURE_TEMPLATE.md @@ -2,4 +2,5 @@ title: '{{ env.TITLE }}' labels: 'Type: Tests, Waiting for: Product Owner' --- + Canary tests failed: {{ env.RUN_LINK }} diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 9c8ca1f159b5..13769041be38 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -34,15 +34,16 @@ body: - '@sentry/astro' - '@sentry/angular' - '@sentry/angular-ivy' + - '@sentry/aws-serverless' - '@sentry/bun' - '@sentry/deno' - '@sentry/ember' - '@sentry/gatsby' + - '@sentry/google-cloud-serverless' - '@sentry/nextjs' - '@sentry/node' - '@sentry/react' - '@sentry/remix' - - '@sentry/serverless' - '@sentry/svelte' - '@sentry/sveltekit' - '@sentry/vue' diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 26b6f695069f..9526e93d55c6 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -36,3 +36,4 @@ jobs: version: ${{ steps.version.outputs.group1 }} force: false merge_target: master + craft_config_from_merge_target: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 954734ad0ba0..9c216581322a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -140,7 +140,6 @@ jobs: - 'packages/profiling-node/**' - 'dev-packages/e2e-tests/test-applications/node-profiling/**' profiling_node_bindings: - - *workflow - 'packages/profiling-node/**' - 'dev-packages/e2e-tests/test-applications/node-profiling/**' deno: @@ -279,39 +278,6 @@ jobs: # `job_build` can't see `job_install_deps` and what it returned) dependency_cache_key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} - job_size_check: - name: Size Check - needs: [job_get_metadata, job_build] - timeout-minutes: 15 - runs-on: ubuntu-20.04 - if: - github.event_name == 'pull_request' || needs.job_get_metadata.outputs.is_develop == 'true' || - needs.job_get_metadata.outputs.is_release == 'true' - steps: - - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v4 - with: - ref: ${{ env.HEAD_COMMIT }} - - name: Set up Node - uses: actions/setup-node@v4 - with: - # The size limit action runs `yarn` and `yarn build` when this job is executed on - # use Node 14 for now. - node-version: '14' - - name: Restore caches - uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Check bundle sizes - uses: getsentry/size-limit-action@runForBranch - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - skip_step: build - main_branch: develop - # When on release branch, we want to always run - # Else, we fall back to the default handling of the action - run_for_branch: ${{ (needs.job_get_metadata.outputs.is_release == 'true' && 'true') || '' }} - job_lint: name: Lint # Even though the linter only checks source code, not built code, it needs the built code in order check that all @@ -336,8 +302,6 @@ jobs: run: yarn lint:lerna - name: Lint C++ files run: yarn lint:clang - - name: Validate ES5 builds - run: yarn validate:es5 job_check_format: name: Check file formatting @@ -627,28 +591,24 @@ jobs: matrix: bundle: - esm - - bundle_es5 - - bundle_es5_min - - bundle_es6 - - bundle_es6_min - - bundle_replay_es6 - - bundle_replay_es6_min - - bundle_tracing_es5 - - bundle_tracing_es5_min - - bundle_tracing_es6 - - bundle_tracing_es6_min - - bundle_tracing_replay_es6 - - bundle_tracing_replay_es6_min + - bundle + - bundle_min + - bundle_replay + - bundle_replay_min + - bundle_tracing + - bundle_tracing_min + - bundle_tracing_replay + - bundle_tracing_replay_min project: - chromium include: # Only check all projects for esm & full bundle # We also shard the tests as they take the longest - - bundle: bundle_tracing_replay_es6_min + - bundle: bundle_tracing_replay_min project: '' shard: 1 shards: 2 - - bundle: bundle_tracing_replay_es6_min + - bundle: bundle_tracing_replay_min project: '' shard: 2 shards: 2 @@ -665,7 +625,7 @@ jobs: shards: 3 exclude: # Do not run the default chromium-only tests - - bundle: bundle_tracing_replay_es6_min + - bundle: bundle_tracing_replay_min project: 'chromium' - bundle: esm project: 'chromium' @@ -911,7 +871,7 @@ jobs: yarn test job_remix_integration_tests: - name: Remix v${{ matrix.remix }} (Node ${{ matrix.node }}) ${{ matrix.tracingIntegration && 'TracingIntegration'}} Tests + name: Remix v${{ matrix.remix }} (Node ${{ matrix.node }}) Tests needs: [job_get_metadata, job_build] if: needs.job_get_metadata.outputs.changed_remix == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04 @@ -919,16 +879,16 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 21] + # For whatever reason, these segfault on Node 18, so we are skipping these for now... + node: [20, 21] remix: [1, 2] # Remix v2 only supports Node 18+, so run Node 14, 16 tests separately include: - node: 14 remix: 1 - - node: 16 - remix: 1 - - tracingIntegration: true - remix: 2 + # For whatever reason, these segfault on Node 16, so we are skipping these for now... + # - node: 16 + # remix: 1 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 @@ -946,7 +906,6 @@ jobs: env: NODE_VERSION: ${{ matrix.node }} REMIX_VERSION: ${{ matrix.remix }} - TRACING_INTEGRATION: ${{ matrix.tracingIntegration }} run: | cd packages/remix yarn test:integration:ci @@ -1057,11 +1016,10 @@ jobs: 'react-create-hash-router', 'react-router-6-use-routes', 'standard-frontend-react', - 'standard-frontend-react-tracing-import', 'sveltekit', 'sveltekit-2', 'generic-ts3.8', - 'node-experimental-fastify-app', + 'node-fastify-app', # TODO(v8): Re-enable hapi tests # 'node-hapi-app', 'node-exports-test-app', diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf4561746bb4..7f36bdbedc05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,3 +29,5 @@ jobs: version: ${{ github.event.inputs.version }} force: ${{ github.event.inputs.force }} merge_target: ${{ github.event.inputs.merge_target }} + craft_config_from_merge_target: true + diff --git a/.size-limit.js b/.size-limit.js index 739c3f62aae1..c984f999e15a 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -3,28 +3,28 @@ module.exports = [ { name: '@sentry/browser (incl. Tracing, Replay, Feedback) - Webpack (gzipped)', path: 'packages/browser/build/npm/esm/index.js', - import: '{ init, Replay, browserTracingIntegration, Feedback }', + import: '{ init, replayIntegration, browserTracingIntegration, feedbackIntegration }', gzip: true, limit: '90 KB', }, { name: '@sentry/browser (incl. Tracing, Replay) - Webpack (gzipped)', path: 'packages/browser/build/npm/esm/index.js', - import: '{ init, Replay, browserTracingIntegration }', + import: '{ init, replayIntegration, browserTracingIntegration }', gzip: true, limit: '75 KB', }, { name: '@sentry/browser (incl. Tracing, Replay with Canvas) - Webpack (gzipped)', path: 'packages/browser/build/npm/esm/index.js', - import: '{ init, Replay, browserTracingIntegration, ReplayCanvas }', + import: '{ init, replayIntegration, browserTracingIntegration, replayCanvasIntegration }', gzip: true, limit: '90 KB', }, { name: '@sentry/browser (incl. Tracing, Replay) - Webpack with treeshaking flags (gzipped)', path: 'packages/browser/build/npm/esm/index.js', - import: '{ init, Replay, browserTracingIntegration }', + import: '{ init, replayIntegration, browserTracingIntegration }', gzip: true, limit: '75 KB', modifyWebpackConfig: function (config) { @@ -55,9 +55,23 @@ module.exports = [ limit: '35 KB', }, { - name: '@sentry/browser (incl. Feedback) - Webpack (gzipped)', + name: '@sentry/browser (incl. feedbackIntegration) - Webpack (gzipped)', path: 'packages/browser/build/npm/esm/index.js', - import: '{ init, Feedback }', + import: '{ init, feedbackIntegration }', + gzip: true, + limit: '50 KB', + }, + { + name: '@sentry/browser (incl. feedbackModalIntegration) - Webpack (gzipped)', + path: 'packages/browser/build/npm/esm/index.js', + import: '{ init, feedbackIntegration, feedbackModalIntegration }', + gzip: true, + limit: '50 KB', + }, + { + name: '@sentry/browser (incl. feedbackScreenshotIntegration) - Webpack (gzipped)', + path: 'packages/browser/build/npm/esm/index.js', + import: '{ init, feedbackIntegration, feedbackModalIntegration, feedbackScreenshotIntegration }', gzip: true, limit: '50 KB', }, @@ -76,69 +90,60 @@ module.exports = [ limit: '28 KB', }, - // Browser CDN bundles (ES6) + // Browser CDN bundles { - name: '@sentry/browser (incl. Tracing, Replay, Feedback) - ES6 CDN Bundle (gzipped)', + name: '@sentry/browser (incl. Tracing, Replay, Feedback) - CDN Bundle (gzipped)', path: 'packages/browser/build/bundles/bundle.tracing.replay.feedback.min.js', gzip: true, limit: '90 KB', }, { - name: '@sentry/browser (incl. Tracing, Replay) - ES6 CDN Bundle (gzipped)', + name: '@sentry/browser (incl. Tracing, Replay) - CDN Bundle (gzipped)', path: 'packages/browser/build/bundles/bundle.tracing.replay.min.js', gzip: true, limit: '75 KB', }, { - name: '@sentry/browser (incl. Tracing) - ES6 CDN Bundle (gzipped)', + name: '@sentry/browser (incl. Tracing) - CDN Bundle (gzipped)', path: 'packages/browser/build/bundles/bundle.tracing.min.js', gzip: true, limit: '37 KB', }, { - name: '@sentry/browser - ES6 CDN Bundle (gzipped)', + name: '@sentry/browser - CDN Bundle (gzipped)', path: 'packages/browser/build/bundles/bundle.min.js', gzip: true, limit: '28 KB', }, - // browser CDN bundles (ES6 + non-gzipped) + // browser CDN bundles (non-gzipped) { - name: '@sentry/browser (incl. Tracing, Replay) - ES6 CDN Bundle (minified & uncompressed)', + name: '@sentry/browser (incl. Tracing, Replay) - CDN Bundle (minified & uncompressed)', path: 'packages/browser/build/bundles/bundle.tracing.replay.min.js', gzip: false, brotli: false, limit: '260 KB', }, { - name: '@sentry/browser (incl. Tracing) - ES6 CDN Bundle (minified & uncompressed)', + name: '@sentry/browser (incl. Tracing) - CDN Bundle (minified & uncompressed)', path: 'packages/browser/build/bundles/bundle.tracing.min.js', gzip: false, brotli: false, limit: '105 KB', }, { - name: '@sentry/browser - ES6 CDN Bundle (minified & uncompressed)', + name: '@sentry/browser - CDN Bundle (minified & uncompressed)', path: 'packages/browser/build/bundles/bundle.min.js', gzip: false, brotli: false, limit: '80 KB', }, - // Browser CDN bundles (ES5) - // Replay is not supported in ES5 mode - { - name: '@sentry/browser (incl. Tracing) - ES5 CDN Bundle (gzipped)', - path: 'packages/browser/build/bundles/bundle.tracing.es5.min.js', - gzip: true, - limit: '40 KB', - }, - // React { name: '@sentry/react (incl. Tracing, Replay) - Webpack (gzipped)', path: 'packages/react/build/esm/index.js', - import: '{ init, browserTracingIntegration, Replay }', + import: '{ init, browserTracingIntegration, replayIntegration }', gzip: true, limit: '75 KB', }, @@ -154,7 +159,7 @@ module.exports = [ { name: '@sentry/nextjs Client (incl. Tracing, Replay) - Webpack (gzipped)', path: 'packages/nextjs/build/esm/client/index.js', - import: '{ init, browserTracingIntegration, Replay }', + import: '{ init, browserTracingIntegration, replayIntegration }', gzip: true, limit: '110 KB', }, @@ -168,7 +173,7 @@ module.exports = [ { name: '@sentry-internal/feedback - Webpack (gzipped)', path: 'packages/feedback/build/npm/esm/index.js', - import: '{ Feedback }', + import: '{ feedbackIntegration }', gzip: true, limit: '25 KB', }, diff --git a/CHANGELOG.md b/CHANGELOG.md index bfb8eacad610..8cb6a635c716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,133 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.0.0-alpha.3 + +This is the third Alpha release of the v8 cycle, which includes a variety of breaking changes. + +Read the [in-depth migration guide](./MIGRATION.md) to find out how to address any breaking changes in your code. + +### Important Changes + +- **feat: Set required node version to >=14.18.0 for all packages (#10968)** + +The minimum Node version required for the SDK is now `14.18.0`. + +- **Serverless SDK Changes** + - feat(google-cloud): Add @sentry/google-cloud package (#10993) + - feat(v8): Add @sentry/aws-serverless package (#11052) + - feat(v8): Rename gcp package to `@sentry/google-cloud-serverless` (#11065) + +`@sentry/serverless` is no longer published, and is replaced by two new packages: `@sentry/google-cloud-serverless` and +`@sentry/aws-serverless`. These packages are now the recommended way to instrument serverless functions. See the +[migration guide](./MIGRATION.md#sentryserverless) for more details. + +- **build(bundles): Use ES2017 for bundles (drop ES5 support) (#10911)** + +The Browser SDK and CDN bundles now emits ES2017 compatible code and drops support for IE11. This also means that the +Browser SDKs (`@sentry/browser`, `@sentry/react`, `@sentry/vue`, etc.) requires the fetch API to be available in the +environment. If you need to support older browsers, please transpile your code to ES5 using babel or similar and add +required polyfills. + +New minimum supported browsers: + +- Chrome 58 +- Edge 15 +- Safari/iOS Safari 11 +- Firefox 54 +- Opera 45 +- Samsung Internet 7.2 + +### Removal/Refactoring of deprecated functionality + +- feat(browser): Remove IE parser from the default stack parsers (#11035) +- feat(bun/v8): Remove all deprecations from Bun SDK (#10971) +- feat(core): Remove `startTransaction` export (#11015) +- feat(v8/core): Move addTracingHeadersToFetchRequest and instrumentFetchRequest to core (#10918) +- feat(v8/deno): Remove deprecations from deno SDK (#10972) +- feat(v8/remix): Remove remixRouterInstrumentation (#10933) +- feat(v8/replay): Opt-in options for `unmask` and `unblock` (#11049) +- feat(v8/tracing): Delete BrowserTracing class (#10919) +- feat(v8/vercel-edge): Remove vercel-edge sdk deprecations (#10974) +- feat(replay/v8): Delete deprecated `replaySession` and `errorSampleRates` (#11045) +- feat(v8): Remove deprecated Replay, Feedback, ReplayCanvas exports (#10814) +- ref: Remove `spanRecorder` and all related code (#10977) +- ref: Remove deprecated `origin` field on span start options (#11058) +- ref: Remove deprecated properties from `startSpan` options (#11054) +- ref(core): Remove `startTransaction` & `finishTransaction` hooks (#11008) +- ref(nextjs): Remove `sentry` field in Next.js config as a means of configuration (#10839) +- ref(nextjs): Remove last internal deprecations (#11019) +- ref(react): Streamline browser tracing integrations & remove old code (#11012) +- ref(svelte): Remove `startChild` deprecations (#10956) +- ref(sveltekit): Update trace propagation & span options (#10838) +- ref(v8/angular): Remove instrumentAngularRouting and fix tests (#11021) + +### Other Changes + +- feat: Ensure `getRootSpan()` does not rely on transaction (#10979) +- feat: Export `getSpanDescendants()` everywhere (#10924) +- feat: Make ESM output valid Node ESM (#10928) +- feat: Remove tags from spans & transactions (#10809) +- feat(angular): Update scope `transactionName` when route is resolved (#11043) +- feat(angular/v8): Change decorator naming and add `name` parameter (#11057) +- feat(astro): Update `@sentry/astro` to use OpenTelemetry (#10960) +- feat(browser): Remove `HttpContext` integration class (#10987) +- feat(browser): Use idle span for browser tracing (#10957) +- feat(build): Allow passing Sucrase options for rollup (#10747) +- feat(build): Core packages into single output files (#11030) +- feat(core): Allow custom tracing implementations (#11003) +- feat(core): Allow metrics aggregator per client (#10949) +- feat(core): Decouple `scope.transactionName` from root spans (#10991) +- feat(core): Ensure trace APIs always return a span (#10942) +- feat(core): Implement `startIdleSpan` (#10934) +- feat(core): Move globals to `__SENTRY__` singleton (#11034) +- feat(core): Move more scope globals to `__SENTRY__` (#11074) +- feat(core): Undeprecate setTransactionName (#10966) +- feat(core): Update `continueTrace` to be callback-only (#11044) +- feat(core): Update `spanToJSON` to handle OTEL spans (#10922) +- feat(deps): bump @sentry/cli from 2.29.1 to 2.30.0 (#11024) +- feat(feedback): New feedback integration with screenshots (#10590) +- feat(nextjs): Bump Webpack Plugin to version 2 and rework config options (#10978) +- feat(nextjs): Support Hybrid Cloud DSNs with `tunnelRoute` option (#10959) +- feat(node): Add `setupFastifyErrorHandler` utility (#11061) +- feat(node): Rephrase wording in http integration JSDoc (#10947) +- feat(opentelemetry): Do not use SentrySpan & Transaction classes (#10982) +- feat(opentelemetry): Remove hub from context (#10983) +- feat(opentelemetry): Use core `getRootSpan` functionality (#11004) +- feat(profiling-node): Refactor deprecated methods & non-hook variant (#10984) +- feat(react): Update scope's `transactionName` in React Router instrumentations (#11048) +- feat(remix): Refactor to use new performance APIs (#10980) +- feat(remix): Update remix SDK to be OTEL-powered (#11031) +- feat(sveltekit): Export `unstable_sentryVitePluginOptions` for full Vite plugin customization (#10930) +- feat(v8/bun): Update @sentry/bun to use OTEL node (#10997) +- fix(ember): Ensure browser tracing is correctly lazy loaded (#11026) +- fix(nextjs): Use passthrough `createReduxEnhancer` on server (#11005) +- fix(node): Add missing core re-exports (#10931) +- fix(node): Correct SDK name (#10961) +- fix(node): Do not assert in vendored proxy code (#11011) +- fix(node): Export spotlightIntegration from OTEL node (#10973) +- fix(node): support undici headers as strings or arrays (#10938) +- fix(opentelemetry): Fix span & sampling propagation (#11092) +- fix(react): Passes the fallback function through React's createElement function (#10623) +- fix(react): Set `handled` value in ErrorBoundary depending on fallback (#10989) +- fix(types): Add `addScopeListener` to `Scope` interface (#10952) +- fix(types): Add `AttachmentType` and use for envelope `attachment_type` property (#10946) +- fix(types): Remove usage of `allowSyntheticDefaultImports` (#11073) +- fix(v8/utils): Stack parser skip frames (not lines of stack string) (#10560) +- ref(angular): Refactor usage of `startChild` (#11056) +- ref(browser): Store browser metrics as attributes instead of tags (#10823) +- ref(browser): Update `scope.transactionName` on pageload and navigation span creation (#10992) +- ref(browser): Update browser metrics to avoid deprecations (#10943) +- ref(browser): Update browser profiling to avoid deprecated APIs (#11007) +- ref(feedback): Move UserFeedback type into feedback.ts (#11032) +- ref(nextjs): Clean up browser tracing integration (#11022) +- ref(node-experimental): Refactor usage of `startChild()` (#11047) +- ref(node): Use new performance APIs in legacy `http` & `undici` (#11055) +- ref(opentelemetry): Remove parent span map (#11014) +- ref(opentelemetry): Remove span metadata handling (#11020) + +Work in this release contributed by @MFoster and @jessezhang91. Thank you for your contributions! + ## 8.0.0-alpha.2 This alpha release fixes a build problem that prevented 8.0.0-alpha.1 from being properly released. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 812aaa870df5..d424a69e1967 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ With that, the repo is fully set up and you are ready to run all commands. Since we are using [`TypeScript`](https://www.typescriptlang.org/), you need to transpile the code to JavaScript to be able to use it. From the top level of the repo, there are three commands available: -- `yarn build:dev`, which runs a one-time build of ES5 and ES6 versions of every package +- `yarn build:dev`, which runs a one-time build of every package - `yarn build:dev:filter `, which runs `yarn build:dev` only in projects relevant to the given package (so, for example, running `yarn build:dev:filter @sentry/react` will build the `react` package, all of its dependencies (`utils`, `core`, `browser`, etc), and all packages which depend on it (currently `gatsby` and `nextjs`)) diff --git a/MIGRATION.md b/MIGRATION.md index 8b5d1bdf0c0e..fda328f7ba42 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,7 +3,7 @@ These docs walk through how to migrate our JavaScript SDKs through different major versions. - Upgrading from [SDK 4.x to 5.x/6.x](./docs/migration/v4-to-v5_v6.md) -- Uprading from [SDK 6.x to 7.x](./docs/migration/v6-to-v7.md) +- Upgrading from [SDK 6.x to 7.x](./docs/migration/v6-to-v7.md) - Upgrading from [SDK 7.x to 8.x](./MIGRATION.md#upgrading-from-7x-to-8x) # Upgrading from 7.x to 8.x @@ -20,22 +20,22 @@ stable release of `8.x` comes out). ## 1. Version Support changes: -**Node.js**: We now official support Node 14.8+ for our CJS package, and Node 18.8+ for our ESM package. This applies to -`@sentry/node` and all of our node-based server-side sdks (`@sentry/nextjs`, `@sentry/serverless`, etc.). We no longer -test against Node 8, 10, or 12 and cannot guarantee that the SDK will work as expected on these versions. +**Node.js**: We now official support Node 14.18+ for our CJS package, and Node 18.8+ for our ESM package. This applies +to `@sentry/node` and all of our node-based server-side sdks (`@sentry/nextjs`, `@sentry/serverless`, etc.). We no +longer test against Node 8, 10, or 12 and cannot guarantee that the SDK will work as expected on these versions. -**Browser**: Our browser SDKs (`@sentry/browser`, `@sentry/react`, `@sentry/vue`, etc.) now require ES6+ compatible +**Browser**: Our browser SDKs (`@sentry/browser`, `@sentry/react`, `@sentry/vue`, etc.) now require ES2017+ compatible browsers. This means that we no longer support IE11 (end of an era). This also means that the Browser SDK requires the fetch API to be available in the environment. New minimum supported browsers: -- Chrome 51 +- Chrome 58 - Edge 15 -- Safari/iOS Safari 10 +- Safari/iOS Safari 11 - Firefox 54 -- Opera 38 -- Samnsung Internet 5 +- Opera 45 +- Samsung Internet 7.2 For IE11 support please transpile your code to ES5 using babel or similar and add required polyfills. @@ -48,6 +48,7 @@ We've removed the following packages: - [@sentry/hub](./MIGRATION.md#sentryhub) - [@sentry/tracing](./MIGRATION.md#sentrytracing) - [@sentry/integrations](./MIGRATION.md#sentryintegrations) +- [@sentry/serverless](./MIGRATION.md#sentryserverless) #### @sentry/hub @@ -62,7 +63,7 @@ We've removed the following packages: For Browser SDKs you can import `BrowserTracing` from the SDK directly: ```js -// Before (v7) +// v7 import * as Sentry from '@sentry/browser'; import { BrowserTracing } from '@sentry/tracing'; @@ -71,8 +72,10 @@ Sentry.init({ tracesSampleRate: 1.0, integrations: [new BrowserTracing()], }); +``` -// After (v8) +```js +// v8 import * as Sentry from '@sentry/browser'; Sentry.init({ @@ -86,7 +89,7 @@ If you were importing `@sentry/tracing` for the side effect, you can now use `Se tracing extensions to the SDK. `addTracingExtensions` replaces the `addExtensionMethods` method from `@sentry/tracing`. ```js -// Before (v7) +// v7 import * as Sentry from '@sentry/browser'; import '@sentry/tracing'; @@ -94,8 +97,10 @@ Sentry.init({ dsn: '__DSN__', tracesSampleRate: 1.0, }); +``` -// After (v8) +```js +// v8 import * as Sentry from '@sentry/browser'; Sentry.addTracingExtensions(); @@ -109,7 +114,7 @@ Sentry.init({ For Node SDKs you no longer need the side effect import, you can remove all references to `@sentry/tracing`. ```js -// Before (v7) +// v7 const Sentry = require('@sentry/node'); require('@sentry/tracing'); @@ -117,8 +122,10 @@ Sentry.init({ dsn: '__DSN__', tracesSampleRate: 1.0, }); +``` -// After (v8) +```js +// v8 const Sentry = require('@sentry/node'); Sentry.init({ @@ -134,10 +141,12 @@ package (`@sentry/integrations`) to `@sentry/browser` and `@sentry/node`. in add classes. ```js -// Before (v7) +// v7 import { RewriteFrames } from '@sentry/integrations'; +``` -// After (v8) +```js +// v8 import { rewriteFramesIntegration } from '@sentry/browser'; ``` @@ -159,11 +168,91 @@ Integrations that are now exported from `@sentry/node` and `@sentry/browser` (or The `Transaction` integration has been removed from `@sentry/integrations`. There is no replacement API. +#### @sentry/serverless + +`@sentry/serverless` has been removed and will no longer be published. The serverless package has been split into two +different packages, `@sentry/aws-serverless` and `@sentry/google-cloud-serverless`. These new packages have smaller +bundle size than `@sentry/serverless`, which should improve your serverless cold-start times. + +`@sentry/aws-serverless` and `@sentry/google-cloud-serverless` has also been changed to only emit CJS builds. The ESM +build for the `@sentry/serverless` package was always broken and we decided to remove it entirely. ESM support will be +re-added at a later date. + +In `@sentry/serverless` you had to use a namespace import to initialize the SDK. This has been removed so that you can +directly import from the SDK instead. + +```js +// v7 +const Sentry = require('@sentry/serverless'); + +Sentry.AWSLambda.init({ + dsn: '__DSN__', + tracesSampleRate: 1.0, +}); + +// v8 +const Sentry = require('@sentry/aws-serverless'); + +Sentry.init({ + dsn: '__DSN__', + tracesSampleRate: 1.0, +}); +``` + +```js +// v7 +const Sentry = require('@sentry/serverless'); + +Sentry.GCPFunction.init({ + dsn: '__DSN__', + tracesSampleRate: 1.0, +}); + +// v8 +const Sentry = require('@sentry/google-cloud-serverless'); + +Sentry.init({ + dsn: '__DSN__', + tracesSampleRate: 1.0, +}); +``` + ## 3. Performance Monitoring Changes +- [Initializing the SDK in v8](./MIGRATION.md/#initializing-the-node-sdk) - [Performance Monitoring API](./MIGRATION.md#performance-monitoring-api) - [Performance Monitoring Integrations](./MIGRATION.md#performance-monitoring-integrations) +### Initializing the Node SDK + +If you are using `@sentry/node` or `@sentry/bun`, or a package that depends on it (`@sentry/nextjs`, `@sentry/remix`, +`@sentry/sveltekit`, `@sentry/`), you will need to initialize the SDK differently. The primary change is to ensure that +the SDK is initialized as early as possible. See [Initializing the SDK in v8](./docs/v8-initializing.md) on what steps +to follow. + +For example with the Remix SDK, you should initialize the SDK at the top of your `entry.server.tsx` server entrypoint +before you do anything else. + +```js +// first import Sentry and initialize Sentry +import * as Sentry from '@sentry/remix'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + tracePropagationTargets: ['example.org'], + // Disabling to test series of envelopes deterministically. + autoSessionTracking: false, +}); + +// then handle everything else +import type { EntryContext } from '@remix-run/node'; +import { RemixServer } from '@remix-run/react'; +import { renderToString } from 'react-dom/server'; + +export const handleError = Sentry.wrapRemixHandleError; +``` + ### Performance Monitoring API The APIs for Performance Monitoring in the SDK have been revamped to align with OpenTelemetry, an open standard for @@ -216,7 +305,7 @@ function middleware(res, req, next) { You can [read more about the new performance APIs here](./docs/v8-new-performance-apis.md). -To accomodate these changes, we're removed the following APIs: +To accommodate these changes, we're removed the following APIs: - [`startTransaction` and `span.startChild`](./MIGRATION.md#deprecate-starttransaction--spanstartchild) - [Certain arguments in `startSpan` and `startTransaction`](./MIGRATION.md#deprecate-arguments-for-startspan-apis) @@ -232,7 +321,7 @@ As we added support for OpenTelemetry, we have expanded the automatic instrument support for frameworks like Fastify, Nest.js, and Hapi, and expanding support for databases like Prisma and MongoDB via Mongoose. -We now support the following integrations out of the box: +We now support the following integrations out of the box without extra configuration: - `httpIntegration`: Automatically instruments Node `http` and `https` standard libraries - `nativeNodeFetchIntegration`: Automatically instruments top level fetch and undici @@ -248,12 +337,16 @@ We now support the following integrations out of the box: - `postgresIntegration`: Automatically instruments PostgreSQL - `prismaIntegration`: Automatically instruments Prisma +To make sure these integrations work properly you'll have to change how you +[initialize the SDK](./docs/v8-initializing.md) + ## 4. Removal of deprecated APIs - [General](./MIGRATION.md#general) - [Browser SDK](./MIGRATION.md#browser-sdk-browser-react-vue-angular-ember-etc) - [Server-side SDKs (Node, Deno, Bun)](./MIGRATION.md#server-side-sdks-node-deno-bun-etc) - [Next.js SDK](./MIGRATION.md#nextjs-sdk) +- [SvelteKit SDK](./MIGRATION.md#sveltekit-sdk) - [Astro SDK](./MIGRATION.md#astro-sdk) ### General @@ -302,13 +395,20 @@ The `getIntegration()` and `getIntegrationById()` have been removed entirely, se [below](./MIGRATION.md#deprecate-getintegration-and-getintegrationbyid). ```js -// Before (v7) +// v7 const replay = Sentry.getIntegration(Replay); +``` -// After (v8) +```js +// v8 const replay = getClient().getIntegrationByName('Replay'); ``` +#### `framesToPop` applies to parsed frames + +Error with `framesToPop` property will have the specified number of frames removed from the top of the stack. This +changes compared to the v7 where the property `framesToPop` was used to remove top n lines from the stack string. + #### `tracingOrigins` has been replaced by `tracePropagationTargets` `tracingOrigins` is now removed in favor of the `tracePropagationTargets` option. The `tracePropagationTargets` option @@ -320,13 +420,15 @@ details. For example for the Browser SDKs: ```ts -// Before (v7) +// v7 Sentry.init({ dsn: '__DSN__', integrations: [new Sentry.BrowserTracing({ tracingOrigins: ['localhost', 'example.com'] })], }); +``` -// After (v8) +```ts +// v8 Sentry.init({ dsn: '__DSN__', integrations: [Sentry.browserTracingIntegration()], @@ -339,8 +441,7 @@ Sentry.init({ The SDKs now support metrics features without any additional configuration. ```ts -// Before (v7) -// Server (Node/Deno/Bun) +// v7 - Server (Node/Deno/Bun) Sentry.init({ dsn: '__DSN__', _experiments: { @@ -348,14 +449,15 @@ Sentry.init({ }, }); -// Before (v7) -// Browser +// v7 - Browser Sentry.init({ dsn: '__DSN__', integrations: [Sentry.metricsAggregatorIntegration()], }); +``` -// After (v8) +```ts +// v8 Sentry.init({ dsn: '__DSN__', }); @@ -367,14 +469,16 @@ In v7 we deprecated the `Severity` enum in favor of using the `SeverityLevel` ty this has been removed in v8. You should now use the `SeverityLevel` type directly. ```js -// Before (v7) +// v7 import { Severity, SeverityLevel } from '@sentry/types'; const levelA = Severity.error; const levelB: SeverityLevel = "error" +``` -// After (v8) +```js +// v8 import { SeverityLevel } from '@sentry/types'; const levelA = "error" as SeverityLevel; @@ -388,12 +492,14 @@ The top level `Sentry.configureScope` function has been removed. Instead, you sh to access and mutate the current scope. ```js -// Before (v7) +// v7 Sentry.configureScope(scope => { scope.setTag('key', 'value'); }); +``` -// After (v8) +```js +// v8 Sentry.getCurrentScope().setTag('key', 'value'); ``` @@ -407,10 +513,12 @@ Internally, this class is now called `SentrySpan`, and it is no longer meant to In v8, we are removing the `spanStatusfromHttpCode` function in favor of `getSpanStatusFromHttpCode`. ```js -// Before (v7) +// v7 const spanStatus = spanStatusfromHttpCode(200); +``` -// After (v8) +```js +// v8 const spanStatus = getSpanStatusFromHttpCode(200); ``` @@ -419,13 +527,15 @@ const spanStatus = getSpanStatusFromHttpCode(200); In v8, we are removing the `addGlobalEventProcessor` function in favor of `addEventProcessor`. ```js -// Before (v7) +// v7 addGlobalEventProcessor(event => { delete event.extra; return event; }); +``` -// After (v8) +```js +// v8 addEventProcessor(event => { delete event.extra; return event; @@ -442,12 +552,14 @@ The `send` method on the `Transport` interface now always requires a `TransportM the promise. This means that the `void` return type is no longer allowed. ```ts -// Before (v7) +// v7 interface Transport { send(event: Event): Promise; } +``` -// After (v8) +```ts +// v8 interface Transport { send(event: Event): Promise; } @@ -518,6 +630,84 @@ The following previously deprecated API has been removed from the `@sentry/nextj - `IS_BUILD` - `isBuild` +#### Merging of the Sentry Webpack Plugin options and SDK Build options + +With version 8 of the Sentry Next.js SDK, `withSentryConfig` will no longer accept 3 arguments. The second argument +(holding options for the Sentry Webpack plugin) and the third argument (holding options for SDK build-time +configuration) should now be passed as one: + +```ts +// OLD +const nextConfig = { + // Your Next.js options... +}; + +module.exports = withSentryConfig( + nextConfig, + { + // Your Sentry Webpack Plugin Options... + }, + { + // Your Sentry SDK options... + }, +); + +// NEW +const nextConfig = { + // Your Next.js options... +}; + +module.exports = withSentryConfig(nextConfig, { + // Your Sentry Webpack Plugin Options... + // AND your Sentry SDK options... +}); +``` + +#### Removal of the `sentry` property in your Next.js options (next.config.js) + +With version 8 of the Sentry Next.js SDK, the SDK will no longer support passing Next.js options with a `sentry` +property to `withSentryConfig`. Please use the second argument of `withSentryConfig` to configure the SDK instead: + +```ts +// v7 +const nextConfig = { + // Your Next.js options... + + sentry: { + // Your Sentry SDK options... + }, +}; + +module.exports = withSentryConfig(nextConfig, { + // Your Sentry Webpack Plugin Options... +}); +``` + +```ts +// v8 +const nextConfig = { + // Your Next.js options... +}; + +module.exports = withSentryConfig(nextConfig, { + // Your Sentry Webpack Plugin Options... + // AND your Sentry SDK options... +}); +``` + +The reason for this change is to have one consistent way of defining the SDK options. We hope that this change will +reduce confusion when setting up the SDK, with the upside that the explicit option is properly typed and will therefore +have code completion. + +#### Updated the `@sentry/webpack-plugin` dependency to version 2 + +We bumped the internal usage of `@sentry/webpack-plugin` to a new major version. This comes with multiple upsides like a +simpler configuration interface and the use of new state of the art Debug ID technology. Debug IDs will simplify the +setup for source maps in Sentry and will not require you to match stack frame paths to uploaded artifacts anymore. + +To see the new options, check out the docs at https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/, +or look at the TypeScript type definitions of `withSentryConfig`. + ### Astro SDK #### Removal of `trackHeaders` option for Astro middleware @@ -537,6 +727,80 @@ Sentry.init({ }); ``` +### SvelteKit SDK + +#### Breaking `sentrySvelteKit()` changes + +We upgraded the `@sentry/vite-plugin` which is a dependency of the SvelteKit SDK from version 0.x to 2.x. With this +change, resolving uploaded source maps should work out of the box much more often than before +([more information](https://docs.sentry.io/platforms/javascript/sourcemaps/troubleshooting_js/artifact-bundles/)). + +To allow future upgrades of the Vite plugin without breaking stable and public APIs in `sentrySvelteKit`, we modified +the `sourceMapsUploadOptions` to remove the hard dependency on the API of the plugin. While you previously could specify +all [version 0.x Vite plugin options](https://www.npmjs.com/package/@sentry/vite-plugin/v/0.6.1), we now reduced them to +a subset of [2.x options](https://www.npmjs.com/package/@sentry/vite-plugin/v/2.14.2#options). All of these options are +optional just like before but here's an example of using the new options. + +```js +// v7 +sentrySvelteKit({ + sourceMapsUploadOptions: { + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + authToken: process.env.SENTRY_AUTH_TOKEN, + release: '1.0.1', + injectRelease: true, + include: ['./build/*/**/*'], + ignore: ['**/build/client/**/*'] + }, +}), +``` + +```js +// v8 +sentrySvelteKit({ + sourceMapsUploadOptions: { + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + authToken: process.env.SENTRY_AUTH_TOKEN, + release: { + name: '1.0.1', + inject: true + }, + sourcemaps: { + assets: ['./build/*/**/*'], + ignore: ['**/build/client/**/*'], + filesToDeleteAfterUpload: ['./build/**/*.map'] + }, + }, +}), +``` + +In the future, we might add additional [options](https://www.npmjs.com/package/@sentry/vite-plugin/v/2.14.2#options) +from the Vite plugin but if you would like to specify some of them directly, you can do this by passing in an +`unstable_sentryVitePluginOptions` object: + +```js +sentrySvelteKit({ + sourceMapsUploadOptions: { + // ... + release: { + name: '1.0.1', + }, + unstable_sentryVitePluginOptions: { + release: { + setCommits: { + auto: true + } + } + } + }, +}), +``` + +Important: we DO NOT guarantee stability of `unstable_sentryVitePluginOptions`. They can be removed or updated at any +time, including breaking changes within the same major version of the SDK. + ## 5. Behaviour Changes - [Updated behaviour of `tracePropagationTargets` in the browser](./MIGRATION.md#updated-behaviour-of-tracepropagationtargets-in-the-browser-http-tracing-headers--cors) @@ -597,6 +861,51 @@ The SDK no longer filters out health check transactions by default. Instead, the by the Sentry backend by default. You can disable dropping them in your Sentry project settings. If you still want to drop specific transactions within the SDK you can either use the `ignoreTransactions` SDK option. +#### Change of Replay default options (`unblock` and `unmask`) + +The Replay options `unblock` and `unmask` now have `[]` as default value. This means that if you want to use these +options, you have to explicitly set them like this: + +```js +Sentry.init({ + integrations: [ + Sentry.replayIntegration({ + unblock: ['.sentry-unblock, [data-sentry-unblock]'], + unmask: ['.sentry-unmask, [data-sentry-unmask]'], + }), + ], +}); +``` + +#### Angular Tracing Decorator renaming + +The usage of `TraceClassDecorator` and the `TraceMethodDecorator` already implies that those are decorators. The word +`Decorator` is now removed from the names to avoid multiple mentioning. + +Additionally, the `TraceClass` and `TraceMethod` decorators accept an optional `name` parameter to set the transaction +name. This was added because Angular minifies class and method names, and you might want to set a more descriptive name. +If nothing provided, the name defaults to `'unnamed'`. + +```js +// v7 +@Sentry.TraceClassDecorator() +export class HeaderComponent { + @Sentry.TraceMethodDecorator() + ngOnChanges(changes: SimpleChanges) {} +} +``` + +```js +// v8 +@Sentry.TraceClass({ name: 'HeaderComponent' }) +export class HeaderComponent { + @Sentry.TraceMethod({ name: 'ngOnChanges' }) + ngOnChanges(changes: SimpleChanges) {} +} +``` + +--- + # Deprecations in 7.x You can use the **Experimental** [@sentry/migr8](https://www.npmjs.com/package/@sentry/migr8) to automatically update @@ -925,10 +1234,6 @@ instead now. Instead, you can get the currently active span via `Sentry.getActiveSpan()`. Setting a span on the scope happens automatically when you use the new performance APIs `startSpan()` and `startSpanManual()`. -## Deprecate `scope.setTransactionName()` - -Instead, either set this as attributes or tags, or use an event processor to set `event.transaction`. - ## Deprecate `scope.getTransaction()` and `getActiveTransaction()` Instead, you should not rely on the active transaction, but just use `startSpan()` APIs, which handle this for you. diff --git a/README.md b/README.md index 807c4b6988e2..f19c4b01c593 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,10 @@ package. Please refer to the README and instructions of those SDKs for more deta - [`@sentry/gatsby`](https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby): SDK for Gatsby - [`@sentry/nextjs`](https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs): SDK for Next.js - [`@sentry/remix`](https://github.com/getsentry/sentry-javascript/tree/master/packages/remix): SDK for Remix -- [`@sentry/serverless`](https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless): SDK for - Serverless Platforms (AWS, GCP) +- [`@sentry/aws-serverless`](https://github.com/getsentry/sentry-javascript/tree/master/packages/aws-serverless): SDK + for AWS Lambda Functions +- [`@sentry/google-cloud-serverless`](https://github.com/getsentry/sentry-javascript/tree/master/packages/google-cloud): + SDK for Google Cloud Functions - [`@sentry/electron`](https://github.com/getsentry/sentry-electron): SDK for Electron with support for native crashes - [`@sentry/react-native`](https://github.com/getsentry/sentry-react-native): SDK for React Native with support for native crashes diff --git a/dev-packages/browser-integration-tests/README.md b/dev-packages/browser-integration-tests/README.md index 6024b98a8085..b24bdfb86ee3 100644 --- a/dev-packages/browser-integration-tests/README.md +++ b/dev-packages/browser-integration-tests/README.md @@ -1,19 +1,29 @@ # Integration Tests for Sentry Browser SDK -Integration tests for Sentry's Browser SDK use [Playwright](https://playwright.dev/) internally. These tests are run on latest stable versions of Chromium, Firefox and Webkit. +Integration tests for Sentry's Browser SDK use [Playwright](https://playwright.dev/) internally. These tests are run on +latest stable versions of Chromium, Firefox and Webkit. ## Structure -The tests are grouped by their scope such as `breadcrumbs` or `onunhandledrejection`. In every group of tests, there are multiple folders containing test cases with their optional supporting assets. +The tests are grouped by their scope such as `breadcrumbs` or `onunhandledrejection`. In every group of tests, there are +multiple folders containing test cases with their optional supporting assets. -Each case group has a default HTML skeleton named `template.hbs`, and also a default initialization script named `init.js `, which contains the `Sentry.init()` call. These defaults are used as fallbacks when a specific `template.hbs` or `init.js` is not defined in a case folder. +Each case group has a default HTML skeleton named `template.hbs`, and also a default initialization script named +`init.js `, which contains the `Sentry.init()` call. These defaults are used as fallbacks when a specific `template.hbs` +or `init.js` is not defined in a case folder. -`subject.js` contains the logic that sets up the environment to be tested. It also can be defined locally and as a group fallback. Unlike `template.hbs` and `init.js`, it's not required to be defined for a group, as there may be cases that does not require a subject, instead the logic is injected using `injectScriptAndGetEvents` from `utils/helpers.ts`. +`subject.js` contains the logic that sets up the environment to be tested. It also can be defined locally and as a group +fallback. Unlike `template.hbs` and `init.js`, it's not required to be defined for a group, as there may be cases that +does not require a subject, instead the logic is injected using `injectScriptAndGetEvents` from `utils/helpers.ts`. -`test.ts` is required for each test case, which contains the assertions (and if required the script injection logic). For every case, any set of `init.js`, `template.hbs` and `subject.js` can be defined locally, and each one of them will have precedence over the default definitions of the test group. +`test.ts` is required for each test case, which contains the assertions (and if required the script injection logic). +For every case, any set of `init.js`, `template.hbs` and `subject.js` can be defined locally, and each one of them will +have precedence over the default definitions of the test group. -To test page multi-page navigations, you can specify additional `page-*.html` (e.g. `page-0.html`, `page-1.html`) files. These will also be compiled and initialized with the same `init.js` and `subject.js` files that are applied to `template.hbs/html`. Note: `page-*.html` file lookup **doesn not** fall back to the -parent directories, meaning that page files have to be directly in the `test.ts` directory. +To test page multi-page navigations, you can specify additional `page-*.html` (e.g. `page-0.html`, `page-1.html`) files. +These will also be compiled and initialized with the same `init.js` and `subject.js` files that are applied to +`template.hbs/html`. Note: `page-*.html` file lookup **doesn not** fall back to the parent directories, meaning that +page files have to be directly in the `test.ts` directory. ``` suites/ @@ -33,11 +43,16 @@ suites/ ### Helpers -`utils/helpers.ts` contains helpers that could be used in assertions (`test.ts`). These helpers define a convenient and reliable API to interact with Playwright's native API. It's highly recommended to define all common patterns of Playwright usage in helpers. +`utils/helpers.ts` contains helpers that could be used in assertions (`test.ts`). These helpers define a convenient and +reliable API to interact with Playwright's native API. It's highly recommended to define all common patterns of +Playwright usage in helpers. ### Fixtures -[Fixtures](https://playwright.dev/docs/api/class-fixtures) allows us to define the globals and test-specific information in assertion groups (`test.ts` files). In it's current state, `fixtures.ts` contains an extension over the pure version of `test()` function of Playwright. All the tests should import `sentryTest` function from `utils/fixtures.ts` instead of `@playwright/test` to be able to access the extra fixtures. +[Fixtures](https://playwright.dev/docs/api/class-fixtures) allows us to define the globals and test-specific information +in assertion groups (`test.ts` files). In it's current state, `fixtures.ts` contains an extension over the pure version +of `test()` function of Playwright. All the tests should import `sentryTest` function from `utils/fixtures.ts` instead +of `@playwright/test` to be able to access the extra fixtures. ## Running Tests Locally @@ -47,8 +62,7 @@ Tests can be run locally using the latest version of Chromium with: To run tests with a different browser such as `firefox` or `webkit`: -`yarn test --project='firefox'` -`yarn test --project='webkit'` +`yarn test --project='firefox'` `yarn test --project='webkit'` Or to run on all three browsers: @@ -60,18 +74,27 @@ To filter tests by their title: You can refer to [Playwright documentation](https://playwright.dev/docs/test-cli) for other CLI options. -You can set env variable `PW_BUNDLE` to set specific build or bundle to test against. -Available options: `esm`, `cjs`, `bundle_es5`, `bundle_es5_min`, `bundle_es6`, `bundle_es6_min` +You can set env variable `PW_BUNDLE` to set specific build or bundle to test against. Available options: `esm`, `cjs`, +`bundle`, `bundle_min` ### Troubleshooting -Apart from [Playwright-specific issues](https://playwright.dev/docs/troubleshooting), below are common issues that might occur while writing tests for Sentry Browser SDK. +Apart from [Playwright-specific issues](https://playwright.dev/docs/troubleshooting), below are common issues that might +occur while writing tests for Sentry Browser SDK. - #### Flaky Tests - If a test fails randomly, giving a `Page Closed`, `Target Closed` or a similar error, most of the times, the reason is a race condition between the page action defined in the `subject` and the listeners of the Sentry event / request. It's recommended to firstly check `utils/helpers.ts` whether if that async logic can be replaced by one of the helpers. If not, whether the awaited (or non-awaited on purpose in some cases) Playwright methods can be orchestrated by [`Promise.all`](http://mdn.io/promise.all). Manually-defined waiting logic such as timeouts are not recommended, and should not be required in most of the cases. + + If a test fails randomly, giving a `Page Closed`, `Target Closed` or a similar error, most of the times, the reason is + a race condition between the page action defined in the `subject` and the listeners of the Sentry event / request. + It's recommended to firstly check `utils/helpers.ts` whether if that async logic can be replaced by one of the + helpers. If not, whether the awaited (or non-awaited on purpose in some cases) Playwright methods can be orchestrated + by [`Promise.all`](http://mdn.io/promise.all). Manually-defined waiting logic such as timeouts are not recommended, + and should not be required in most of the cases. - #### Build Errors - Before running, a page for each test case is built under the case folder inside `dist`. If a page build is failed, it's recommended to check: + + Before running, a page for each test case is built under the case folder inside `dist`. If a page build is failed, + it's recommended to check: - If both default `template.hbs` and `init.js` are defined for the test group. - If a `subject.js` is defined for the test case. diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js index 921209ce14dc..150a9f6a20ae 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js +++ b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js @@ -2,7 +2,7 @@ Sentry.onLoad(function () { Sentry.init({ integrations: [ // Without this syntax, this will be re-written by the test framework - new window['Sentry'].Replay({ + window['Sentry'].replayIntegration({ useCompression: false, }), ], diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 55697825c3a1..1daa3be5e5e7 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -4,7 +4,7 @@ "main": "index.js", "license": "MIT", "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "private": true, "scripts": { @@ -17,18 +17,12 @@ "pretest": "yarn clean && yarn type-check", "test": "yarn test:all --project='chromium'", "test:all": "npx playwright test -c playwright.browser.config.ts", - "test:bundle:es5": "PW_BUNDLE=bundle_es5 yarn test", - "test:bundle:es5:min": "PW_BUNDLE=bundle_es5_min yarn test", - "test:bundle:es6": "PW_BUNDLE=bundle_es6 yarn test", - "test:bundle:es6:min": "PW_BUNDLE=bundle_es6_min yarn test", - "test:bundle:replay:es6": "PW_BUNDLE=bundle_replay_es6 yarn test", - "test:bundle:replay:es6:min": "PW_BUNDLE=bundle_replay_es6_min yarn test", - "test:bundle:tracing:es5": "PW_BUNDLE=bundle_tracing_es5 yarn test", - "test:bundle:tracing:es5:min": "PW_BUNDLE=bundle_tracing_es5_min yarn test", - "test:bundle:tracing:es6": "PW_BUNDLE=bundle_tracing_es6 yarn test", - "test:bundle:tracing:es6:min": "PW_BUNDLE=bundle_tracing_es6_min yarn test", - "test:bundle:tracing:replay:es6": "PW_BUNDLE=bundle_tracing_replay_es6 yarn test", - "test:bundle:tracing:replay:es6:min": "PW_BUNDLE=bundle_tracing_replay_es6_min yarn test", + "test:bundle": "PW_BUNDLE=bundle yarn test", + "test:bundle:min": "PW_BUNDLE=bundle_min yarn test", + "test:bundle:replay": "PW_BUNDLE=bundle_replay yarn test", + "test:bundle:replay:min": "PW_BUNDLE=bundle_replay_min yarn test", + "test:bundle:tracing": "PW_BUNDLE=bundle_tracing yarn test", + "test:bundle:tracing:min": "PW_BUNDLE=bundle_tracing_min yarn test", "test:cjs": "PW_BUNDLE=cjs yarn test", "test:esm": "PW_BUNDLE=esm yarn test", "test:loader": "npx playwright test -c playwright.loader.config.ts --project='chromium'", @@ -40,8 +34,7 @@ "test:loader:debug": "PW_BUNDLE=loader_debug yarn test:loader", "test:ci": "yarn test:all --reporter='line'", "test:update-snapshots": "yarn test:all --update-snapshots", - "test:detect-flaky": "ts-node scripts/detectFlakyTests.ts", - "validate:es5": "es-check es5 'fixtures/loader.js'" + "test:detect-flaky": "ts-node scripts/detectFlakyTests.ts" }, "dependencies": { "@babel/preset-typescript": "^7.16.7", diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js index e64314facd79..6455e8d8851a 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js @@ -1,9 +1,8 @@ -import { Feedback } from '@sentry-internal/feedback'; import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [new Feedback()], + integrations: [Sentry.feedbackIntegration()], }); diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js index 9428907f51e7..46441bdf2538 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js @@ -1,4 +1,3 @@ -import { Feedback } from '@sentry-internal/feedback'; import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; @@ -8,11 +7,11 @@ Sentry.init({ replaysOnErrorSampleRate: 1.0, replaysSessionSampleRate: 0, integrations: [ - new Sentry.Replay({ + Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, }), - new Feedback(), + Sentry.feedbackIntegration(), ], }); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/browser-context/init.js b/dev-packages/browser-integration-tests/suites/manual-client/browser-context/init.js index 33777734fe84..4066402e4e15 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/browser-context/init.js +++ b/dev-packages/browser-integration-tests/suites/manual-client/browser-context/init.js @@ -29,7 +29,6 @@ const client = new BrowserClient({ transport: makeFetchTransport, stackParser: defaultStackParser, integrations, - debug: true, }); const hub = new Hub(client); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js index 573e4fdcb621..0223ee8568b4 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js @@ -4,5 +4,6 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', + // We assert on the debug message, so we need to enable debug mode debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js index aaf2de868f6f..35d41b55e69d 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js @@ -3,6 +3,9 @@ async function run() { Sentry.startSpan({ name: 'child_span' }, () => { // whatever a user would do here }); + + // unfinished spans are filtered out + Sentry.startInactiveSpan({ name: 'span_4' }); }); } diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts index 71637c9294f1..f1152bde21da 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts @@ -1,8 +1,11 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; sentryTest('should send a transaction in an envelope', async ({ getLocalTestPath, page }) => { if (shouldSkipTracingTest()) { @@ -10,7 +13,8 @@ sentryTest('should send a transaction in an envelope', async ({ getLocalTestPath } const url = await getLocalTestPath({ testDir: __dirname }); - const transaction = await getFirstSentryEnvelopeRequest(page, url); + const req = await waitForTransactionRequestOnUrl(page, url); + const transaction = envelopeRequestParser(req); expect(transaction.transaction).toBe('parent_span'); expect(transaction.spans).toBeDefined(); @@ -22,7 +26,8 @@ sentryTest('should report finished spans as children of the root transaction', a } const url = await getLocalTestPath({ testDir: __dirname }); - const transaction = await getFirstSentryEnvelopeRequest(page, url); + const req = await waitForTransactionRequestOnUrl(page, url); + const transaction = envelopeRequestParser(req); expect(transaction.spans).toHaveLength(1); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js rename to dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/subject.js diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/test.ts rename to dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/init.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/init.js index 5724df357e8b..3fb0df7a75d4 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/init.js @@ -8,5 +8,4 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, normalizeDepth: 10, - debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/subject.js similarity index 68% rename from dev-packages/browser-integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js rename to dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/subject.js index 5b14dd7b680b..9316ed946b2d 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/subject.js @@ -1,4 +1,7 @@ -const transaction = Sentry.startTransaction({ name: 'some_transaction' }); +const transaction = Sentry.startInactiveSpan({ + name: 'some_transaction', + forceTransaction: true, +}); transaction.setMeasurement('metric.foo', 42, 'ms'); transaction.setMeasurement('metric.bar', 1337, 'nanoseconds'); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts rename to dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/subject.js deleted file mode 100644 index 5942deca663b..000000000000 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/subject.js +++ /dev/null @@ -1,23 +0,0 @@ -Sentry.startSpan({ name: 'root_span' }, () => { - Sentry.startSpan( - { - name: 'span_1', - data: { - foo: 'bar', - baz: [1, 2, 3], - }, - }, - () => undefined, - ); - - // span_2 doesn't finish - Sentry.startInactiveSpan({ name: 'span_2' }); - - Sentry.startSpan({ name: 'span_3' }, () => { - // span_4 is the child of span_3 but doesn't finish. - Sentry.startInactiveSpan({ name: 'span_4', data: { qux: 'quux' } }); - - // span_5 is another child of span_3 but finishes. - Sentry.startInactiveSpan({ name: 'span_5' }).end(); - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts deleted file mode 100644 index 0624ff3f7dad..000000000000 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { expect } from '@playwright/test'; -import type { Event } from '@sentry/types'; - -import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest('should report a transaction in an envelope', async ({ getLocalTestPath, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestPath({ testDir: __dirname }); - const transaction = await getFirstSentryEnvelopeRequest(page, url); - - expect(transaction.transaction).toBe('root_span'); - expect(transaction.spans).toBeDefined(); -}); - -sentryTest('should report finished spans as children of the root span', async ({ getLocalTestPath, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestPath({ testDir: __dirname }); - const transaction = await getFirstSentryEnvelopeRequest(page, url); - - const rootSpanId = transaction?.contexts?.trace?.span_id; - - expect(transaction.spans).toHaveLength(3); - - const span_1 = transaction.spans?.[0]; - - expect(span_1).toBeDefined(); - expect(span_1!.description).toBe('span_1'); - expect(span_1!.parent_span_id).toEqual(rootSpanId); - expect(span_1!.data).toMatchObject({ foo: 'bar', baz: [1, 2, 3] }); - - const span_3 = transaction.spans?.[1]; - expect(span_3).toBeDefined(); - expect(span_3!.description).toBe('span_3'); - expect(span_3!.parent_span_id).toEqual(rootSpanId); - - const span_5 = transaction.spans?.[2]; - expect(span_5).toBeDefined(); - expect(span_5!.description).toBe('span_5'); - expect(span_5!.parent_span_id).toEqual(span_3!.span_id); -}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/init.js b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/init.js deleted file mode 100644 index 3fb0df7a75d4..000000000000 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/init.js +++ /dev/null @@ -1,11 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.addTracingExtensions(); - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - tracesSampleRate: 1.0, - normalizeDepth: 10, -}); diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferMode/init.js b/dev-packages/browser-integration-tests/suites/replay/bufferMode/init.js index 2453efcfbe1d..dfc142feaa09 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferMode/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/bufferMode/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferMode/test.ts b/dev-packages/browser-integration-tests/suites/replay/bufferMode/test.ts index 1ed90b81b37a..9c10b6777d67 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferMode/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/bufferMode/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import type { Replay } from '@sentry/replay'; +import type { replayIntegration as actualReplayIntegration } from '@sentry/replay'; import type { ReplayContainer } from '@sentry/replay/build/npm/types/types'; import { sentryTest } from '../../../utils/fixtures'; @@ -58,8 +58,9 @@ sentryTest( expect( await page.evaluate(() => { - const replayIntegration = (window as unknown as Window & { Replay: { _replay: ReplayContainer } }).Replay; - const replay = replayIntegration._replay; + const replayIntegration = (window as unknown as Window & { Replay: ReturnType }) + .Replay; + const replay = replayIntegration['_replay']; return replay.isEnabled(); }), ).toBe(false); @@ -67,10 +68,9 @@ sentryTest( // Start buffering and assert that it is enabled expect( await page.evaluate(() => { - // eslint-disable-next-line deprecation/deprecation - const replayIntegration = (window as unknown as Window & { Replay: InstanceType }).Replay; - // @ts-expect-error private - const replay = replayIntegration._replay; + const replayIntegration = (window as unknown as Window & { Replay: ReturnType }) + .Replay; + const replay = replayIntegration['_replay']; replayIntegration.startBuffering(); return replay.isEnabled(); }), @@ -88,8 +88,8 @@ sentryTest( const [req0] = await Promise.all([ reqPromise0, page.evaluate(async () => { - // eslint-disable-next-line deprecation/deprecation - const replayIntegration = (window as unknown as Window & { Replay: Replay }).Replay; + const replayIntegration = (window as unknown as Window & { Replay: ReturnType }) + .Replay; await replayIntegration.flush(); }), ]); @@ -212,10 +212,9 @@ sentryTest( // Start buffering and assert that it is enabled expect( await page.evaluate(() => { - // eslint-disable-next-line deprecation/deprecation - const replayIntegration = (window as unknown as Window & { Replay: InstanceType }).Replay; - // @ts-expect-error private - const replay = replayIntegration._replay; + const replayIntegration = (window as unknown as Window & { Replay: ReturnType }) + .Replay; + const replay = replayIntegration['_replay']; replayIntegration.startBuffering(); return replay.isEnabled(); }), @@ -233,8 +232,8 @@ sentryTest( const [req0] = await Promise.all([ reqPromise0, page.evaluate(async () => { - // eslint-disable-next-line deprecation/deprecation - const replayIntegration = (window as unknown as Window & { Replay: Replay }).Replay; + const replayIntegration = (window as unknown as Window & { Replay: ReturnType }) + .Replay; await replayIntegration.flush({ continueRecording: false }); }), ]); @@ -328,8 +327,8 @@ sentryTest( // Start buffering and assert that it is enabled expect( await page.evaluate(() => { - // eslint-disable-next-line deprecation/deprecation - const replayIntegration = (window as unknown as Window & { Replay: InstanceType }).Replay; + const replayIntegration = (window as unknown as Window & { Replay: ReturnType }) + .Replay; const replay = replayIntegration['_replay']; replayIntegration.startBuffering(); return replay.isEnabled(); @@ -347,8 +346,8 @@ sentryTest( expect(errorEvent0.tags?.replayId).toBeUndefined(); await page.evaluate(async () => { - // eslint-disable-next-line deprecation/deprecation - const replayIntegration = (window as unknown as Window & { Replay: Replay }).Replay; + const replayIntegration = (window as unknown as Window & { Replay: ReturnType }) + .Replay; replayIntegration['_replay'].getOptions().errorSampleRate = 1.0; }); diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js b/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js index 89c185dacc7f..7024a6574324 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js index 244f951588c6..7704e9934cbf 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 50, flushMaxDelay: 50, minReplayDuration: 0, @@ -12,7 +12,6 @@ Sentry.init({ sampleRate: 0, replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 0.0, - debug: true, - integrations: [window.Replay, new Sentry.ReplayCanvas({ enableManualSnapshot: true })], + integrations: [window.Replay, Sentry.replayCanvasIntegration({ enableManualSnapshot: true })], }); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js index 296245b01c26..7bea93df6756 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 50, flushMaxDelay: 50, minReplayDuration: 0, @@ -12,7 +12,6 @@ Sentry.init({ sampleRate: 0, replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 0.0, - debug: true, - integrations: [window.Replay, new Sentry.ReplayCanvas()], + integrations: [window.Replay, Sentry.replayCanvasIntegration()], }); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js index fa3248066150..d933111eb57e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, @@ -12,7 +12,6 @@ Sentry.init({ sampleRate: 0, replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 0.0, - debug: true, - integrations: [new Sentry.ReplayCanvas(), window.Replay], + integrations: [Sentry.replayCanvasIntegration(), window.Replay], }); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js index 1a9d5d179c4a..f04edea57625 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, @@ -12,7 +12,6 @@ Sentry.init({ sampleRate: 0, replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 0.0, - debug: true, - integrations: [window.Replay, new Sentry.ReplayCanvas()], + integrations: [window.Replay, Sentry.replayCanvasIntegration()], }); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js index 92a463a4bc84..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, @@ -12,7 +12,6 @@ Sentry.init({ sampleRate: 0, replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 0.0, - debug: true, integrations: [window.Replay], }); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js index 040fb88ab2bd..f8ffa20ceb8f 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js index be0f9cab95d5..f71ddfdaaa9b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js @@ -1,8 +1,8 @@ import * as Sentry from '@sentry/browser'; -import { Replay } from '@sentry/replay'; +import { replayIntegration } from '@sentry/replay'; window.Sentry = Sentry; -window.Replay = new Replay({ +window.Replay = replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js b/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js index db9828fe889e..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js b/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js index c2dd47ab0c25..ab1e26f4490a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js b/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js index 0d0e7ac0e71e..973a0ae3efdc 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js b/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js index f76a1207243b..2cfec5213d3f 100644 --- a/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js @@ -1,12 +1,13 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, useCompression: false, blockAllMedia: false, + unmask: ['.sentry-unmask, [data-sentry-unmask]'], }); Sentry.init({ diff --git a/dev-packages/browser-integration-tests/suites/replay/dsc/init.js b/dev-packages/browser-integration-tests/suites/replay/dsc/init.js index a686fdc78205..6411031d12b5 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, 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 c6483e2cdea0..e46c958497f6 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts @@ -8,8 +8,7 @@ import { getReplaySnapshot, shouldSkipReplayTest, waitForReplayRunning } from '. type TestWindow = Window & { Sentry: typeof Sentry; - // eslint-disable-next-line deprecation/deprecation - Replay: Sentry.Replay; + Replay: ReturnType; }; sentryTest( diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js index a4d39ad78e7e..83e514016c1b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js index 2c9cd8b23147..c24b8e1daca8 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js new file mode 100644 index 000000000000..5e71c58fc0aa --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = Sentry.replayIntegration({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1, + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, + integrations: [window.Replay], + transport: options => { + const transport = new Sentry.makeFetchTransport(options); + + delete transport.send.__sentry__baseTransport__; + + return transport; + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js index 49d938b15060..7ab704b50d6a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js index 3bc87bdd0be4..a765fc48c67b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/init.js index 89c185dacc7f..7024a6574324 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js index 21c548a5e349..c43d5713f173 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js index 4c3f0a7969c6..8e8006759bee 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js index 3aa81d299ae2..f2eb62b8c051 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js index ff1e66e53411..a2a7a1680123 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts index c587db401e4f..8f098627c120 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts @@ -38,12 +38,10 @@ sentryTest('handles empty headers', async ({ getLocalTestPath, page, browserName await page.goto(url); await page.evaluate(() => { - /* eslint-disable */ fetch('http://localhost:7654/foo').then(() => { // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); - /* eslint-enable */ }); const request = await requestPromise; @@ -114,12 +112,10 @@ sentryTest('captures response headers', async ({ getLocalTestPath, page }) => { await page.goto(url); await page.evaluate(() => { - /* eslint-disable */ fetch('http://localhost:7654/foo').then(() => { // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); - /* eslint-enable */ }); const request = await requestPromise; @@ -196,12 +192,10 @@ sentryTest('does not capture response headers if URL does not match', async ({ g await page.goto(url); await page.evaluate(() => { - /* eslint-disable */ fetch('http://localhost:7654/bar').then(() => { // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); - /* eslint-enable */ }); const request = await requestPromise; diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js index 52c219e99dc9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js index 52c219e99dc9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js index 21c548a5e349..c43d5713f173 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js index 4c3f0a7969c6..8e8006759bee 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js index 3aa81d299ae2..f2eb62b8c051 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js index ff1e66e53411..a2a7a1680123 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js index 52c219e99dc9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js index 52c219e99dc9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js b/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js index 0e08fdfaa6d0..c973a578baae 100644 --- a/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/flushing/init.js b/dev-packages/browser-integration-tests/suites/replay/flushing/init.js index db9828fe889e..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/flushing/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/flushing/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/init.js b/dev-packages/browser-integration-tests/suites/replay/init.js index dac512988b9a..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js b/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js index dac512988b9a..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js b/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js index dac512988b9a..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js b/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js index ed46fe5974dc..d09fe6530e9c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js b/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js index 140b486c5755..7bab07447b3a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js b/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js index cff168651bea..2a3dae73541d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 2000, diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js index a856a0d13c3e..f5a01ece0a37 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js index f5360c53561b..16000b4c4412 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js @@ -1,13 +1,14 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, useCompression: false, blockAllMedia: false, block: ['link[rel="icon"]', 'video', '.nested-hide'], + unmask: ['.sentry-unmask, [data-sentry-unmask]'], }); Sentry.init({ diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js index db9828fe889e..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/template.html b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/template.html index c83b62bf2e24..efb00dc29581 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/template.html +++ b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/template.html @@ -7,9 +7,9 @@
This should be masked by default
-
This should be unmasked due to data attribute
+
With default settings, this should also be masked (even with data attribute)
- +
Title should be masked
diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-chromium.json b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-chromium.json index 69f74ba00da8..e04944384bbd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-chromium.json +++ b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-chromium.json @@ -106,7 +106,7 @@ "childNodes": [ { "type": 3, - "textContent": "This should be unmasked due to data attribute", + "textContent": "**** ******* ********* **** ****** **** ** ****** ***** **** **** **********", "id": 17 } ], @@ -136,7 +136,7 @@ "tagName": "input", "attributes": { "data-sentry-unmask": "", - "placeholder": "Placeholder can be unmasked" + "placeholder": "*********** *** ** ******** * *** *** **** ******* ********" }, "childNodes": [], "id": 21 @@ -186,45 +186,17 @@ "type": 2, "tagName": "svg", "attributes": { - "style": "width:200px;height:200px", - "viewBox": "0 0 80 80", - "data-sentry-unblock": "" + "rr_width": "[200-250]px", + "rr_height": "[200-250]px" }, - "childNodes": [ - { - "type": 2, - "tagName": "path", - "attributes": { - "d": "" - }, - "childNodes": [], - "isSVG": true, - "id": 29 - }, - { - "type": 2, - "tagName": "area", - "attributes": {}, - "childNodes": [], - "isSVG": true, - "id": 30 - }, - { - "type": 2, - "tagName": "rect", - "attributes": {}, - "childNodes": [], - "isSVG": true, - "id": 31 - } - ], + "childNodes": [], "isSVG": true, "id": 28 }, { "type": 3, "textContent": "\n ", - "id": 32 + "id": 29 }, { "type": 2, @@ -234,28 +206,27 @@ "rr_height": "[100-150]px" }, "childNodes": [], - "id": 33 + "id": 30 }, { "type": 3, "textContent": "\n ", - "id": 34 + "id": 31 }, { "type": 2, "tagName": "img", "attributes": { - "data-sentry-unblock": "", - "style": "width:100px;height:100px", - "src": "file:///none.png" + "rr_width": "[100-150]px", + "rr_height": "[100-150]px" }, "childNodes": [], - "id": 35 + "id": 32 }, { "type": 3, "textContent": "\n ", - "id": 36 + "id": 33 }, { "type": 2, @@ -265,17 +236,17 @@ "rr_height": "[0-50]px" }, "childNodes": [], - "id": 37 + "id": 34 }, { "type": 3, "textContent": "\n ", - "id": 38 + "id": 35 }, { "type": 3, "textContent": "\n\n", - "id": 39 + "id": 36 } ], "id": 8 diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-firefox.json b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-firefox.json index 4f20b93e13ab..a57a8507fda9 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-firefox.json +++ b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-firefox.json @@ -106,7 +106,7 @@ "childNodes": [ { "type": 3, - "textContent": "This should be unmasked due to data attribute", + "textContent": "**** ******* ********* **** ****** **** ** ****** ***** **** **** **********", "id": 17 } ], @@ -121,7 +121,7 @@ "type": 2, "tagName": "input", "attributes": { - "placeholder": "*********** ****** ** ******" + "placeholder": "*********** *** ** ******** * *** *** **** ******* ********" }, "childNodes": [], "id": 19 @@ -226,12 +226,12 @@ { "type": 3, "textContent": "\n ", - "id": 33 + "id": 34 }, { "type": 3, "textContent": "\n\n", - "id": 34 + "id": 35 } ], "id": 8 @@ -248,4 +248,4 @@ } }, "timestamp": [timestamp] -} \ No newline at end of file +} diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-webkit.json b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-webkit.json index 69f74ba00da8..e04944384bbd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-webkit.json +++ b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-webkit.json @@ -106,7 +106,7 @@ "childNodes": [ { "type": 3, - "textContent": "This should be unmasked due to data attribute", + "textContent": "**** ******* ********* **** ****** **** ** ****** ***** **** **** **********", "id": 17 } ], @@ -136,7 +136,7 @@ "tagName": "input", "attributes": { "data-sentry-unmask": "", - "placeholder": "Placeholder can be unmasked" + "placeholder": "*********** *** ** ******** * *** *** **** ******* ********" }, "childNodes": [], "id": 21 @@ -186,45 +186,17 @@ "type": 2, "tagName": "svg", "attributes": { - "style": "width:200px;height:200px", - "viewBox": "0 0 80 80", - "data-sentry-unblock": "" + "rr_width": "[200-250]px", + "rr_height": "[200-250]px" }, - "childNodes": [ - { - "type": 2, - "tagName": "path", - "attributes": { - "d": "" - }, - "childNodes": [], - "isSVG": true, - "id": 29 - }, - { - "type": 2, - "tagName": "area", - "attributes": {}, - "childNodes": [], - "isSVG": true, - "id": 30 - }, - { - "type": 2, - "tagName": "rect", - "attributes": {}, - "childNodes": [], - "isSVG": true, - "id": 31 - } - ], + "childNodes": [], "isSVG": true, "id": 28 }, { "type": 3, "textContent": "\n ", - "id": 32 + "id": 29 }, { "type": 2, @@ -234,28 +206,27 @@ "rr_height": "[100-150]px" }, "childNodes": [], - "id": 33 + "id": 30 }, { "type": 3, "textContent": "\n ", - "id": 34 + "id": 31 }, { "type": 2, "tagName": "img", "attributes": { - "data-sentry-unblock": "", - "style": "width:100px;height:100px", - "src": "file:///none.png" + "rr_width": "[100-150]px", + "rr_height": "[100-150]px" }, "childNodes": [], - "id": 35 + "id": 32 }, { "type": 3, "textContent": "\n ", - "id": 36 + "id": 33 }, { "type": 2, @@ -265,17 +236,17 @@ "rr_height": "[0-50]px" }, "childNodes": [], - "id": 37 + "id": 34 }, { "type": 3, "textContent": "\n ", - "id": 38 + "id": 35 }, { "type": 3, "textContent": "\n\n", - "id": 39 + "id": 36 } ], "id": 8 diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json index 69f74ba00da8..16c4caf2ed69 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json +++ b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json @@ -106,7 +106,7 @@ "childNodes": [ { "type": 3, - "textContent": "This should be unmasked due to data attribute", + "textContent": "**** ******* ********* **** ****** **** ** ****** ***** **** **** **********", "id": 17 } ], @@ -136,7 +136,7 @@ "tagName": "input", "attributes": { "data-sentry-unmask": "", - "placeholder": "Placeholder can be unmasked" + "placeholder": "*********** *** ** ******** * *** *** **** ******* ********" }, "childNodes": [], "id": 21 @@ -292,4 +292,4 @@ } }, "timestamp": [timestamp] -} \ No newline at end of file +} diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js index 0e08fdfaa6d0..96f5c063e13d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js @@ -1,12 +1,13 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, useCompression: false, maskAllInputs: false, + unmask: ['.sentry-unmask, [data-sentry-unmask]'], }); Sentry.init({ diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js index 1657e879ef87..c20c8b36a4c2 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js @@ -1,12 +1,13 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, useCompression: false, maskAllInputs: true, + unmask: ['.sentry-unmask, [data-sentry-unmask]'], }); Sentry.init({ diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts index 6817367ee68d..952c841b253e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts @@ -30,6 +30,8 @@ sentryTest( await forceFlushReplay(); expect(requestCount).toBe(0); - expect(consoleMessages).toEqual(['You are using new Replay() even though this bundle does not include replay.']); + expect(consoleMessages).toEqual([ + 'You are using replayIntegration() even though this bundle does not include replay.', + ]); }, ); diff --git a/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js b/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js index a89c744e7480..77fc89c70a25 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js @@ -3,7 +3,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; // Replay should not actually work, but still not error out -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts b/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts index 5cb297551dfe..b906deefb71b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts @@ -30,6 +30,8 @@ sentryTest( await forceFlushReplay(); expect(requestCount).toBe(0); - expect(consoleMessages).toEqual(['You are using new Replay() even though this bundle does not include replay.']); + expect(consoleMessages).toEqual([ + 'You are using replayIntegration() even though this bundle does not include replay.', + ]); }, ); diff --git a/dev-packages/browser-integration-tests/suites/replay/requests/init.js b/dev-packages/browser-integration-tests/suites/replay/requests/init.js index db9828fe889e..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/requests/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/requests/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sampling/init.js b/dev-packages/browser-integration-tests/suites/replay/sampling/init.js index 8a98bb9c45de..6405faaf741b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sampling/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sampling/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js b/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js index 6fa2c80cbe9c..ee55b5cca2c4 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js b/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js index c37968bc654a..874580a83eff 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js b/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js index 4b1e948f534e..214340ba12c5 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js b/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js index aa5be4406824..b21921ae5726 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js b/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js index b253a8bca6e9..77b6bf6b67e4 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js b/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js index 575e490b2927..5083bd48c745 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js b/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js index 3146e64131fd..6f2a54514433 100644 --- a/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 5000, flushMaxDelay: 5000, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js b/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js index 2fe6781ee15e..4d4476012c0d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, diff --git a/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js b/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js index 956586937a19..cc0094a0366b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = Sentry.replayIntegration({ flushMinDelay: 200, flushMaxDelay: 200, minReplayDuration: 0, 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 index c22f576ca681..fc0eb65be166 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js @@ -12,5 +12,4 @@ Sentry.init({ username: 'user1337', }, }, - debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/error/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/error/subject.js new file mode 100644 index 000000000000..2bb2bba64d9e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/error/subject.js @@ -0,0 +1,3 @@ +setTimeout(() => { + throw new Error('Error during pageload'); +}, 100); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/error/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/error/test.ts new file mode 100644 index 000000000000..df684872e7c0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/error/test.ts @@ -0,0 +1,30 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'should put the pageload transaction name onto an error event caught during pageload', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + + const [e1, e2] = await getMultipleSentryEnvelopeRequests(page, 2); + + const pageloadTxnEvent = e1.type === 'transaction' ? e1 : e2; + const errorEvent = e1.type === 'transaction' ? e2 : e1; + + expect(pageloadTxnEvent.contexts?.trace?.op).toEqual('pageload'); + expect(pageloadTxnEvent.spans?.length).toBeGreaterThan(0); + expect(errorEvent.exception?.values?.[0]).toBeDefined(); + + expect(pageloadTxnEvent.transaction?.endsWith('index.html')).toBe(true); + + expect(errorEvent.transaction).toEqual(pageloadTxnEvent.transaction); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/init.js new file mode 100644 index 000000000000..98e297d13625 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/init.js @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + // To avoid having this test run for 15s + childSpanTimeout: 4000, + }), + ], + defaultIntegrations: false, + tracesSampleRate: 1, +}); + +const activeSpan = Sentry.getActiveSpan(); +if (activeSpan) { + Sentry.startInactiveSpan({ name: 'pageload-child-span' }); +} else { + setTimeout(() => { + Sentry.startInactiveSpan({ name: 'pageload-child-span' }); + }, 200); +} diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/test.ts new file mode 100644 index 000000000000..5987061c741f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/test.ts @@ -0,0 +1,28 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; + +// This tests asserts that the pageload span will finish itself after the child span timeout if it +// has a child span without adding any additional ones or finishing any of them finishing. All of the child spans that +// are still running should have the status "cancelled". +sentryTest('should send a pageload span terminated via child span timeout', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForTransactionRequestOnUrl(page, url); + + const eventData = envelopeRequestParser(req); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThanOrEqual(1); + const testSpan = eventData.spans?.find(span => span.description === 'pageload-child-span'); + expect(testSpan).toBeDefined(); + expect(testSpan?.status).toBe('cancelled'); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js deleted file mode 100644 index 8b12fe807d7b..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js +++ /dev/null @@ -1,14 +0,0 @@ -import * as Sentry from '@sentry/browser'; -import { startSpanManual } from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration()], - tracesSampleRate: 1, -}); - -setTimeout(() => { - startSpanManual({ name: 'pageload-child-span' }, () => {}); -}, 200); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts deleted file mode 100644 index f96495a69925..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect } from '@playwright/test'; -import type { Event } from '@sentry/types'; - -import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; - -// This tests asserts that the pageload transaction will finish itself after about 15 seconds (3x5s of heartbeats) if it -// has a child span without adding any additional ones or finishing any of them finishing. All of the child spans that -// are still running should have the status "cancelled". -sentryTest( - 'should send a pageload transaction terminated via heartbeat timeout', - async ({ getLocalTestPath, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestPath({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.contexts?.trace?.op).toBe('pageload'); - expect( - eventData.spans?.find(span => span.description === 'pageload-child-span' && span.status === 'cancelled'), - ).toBeDefined(); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts index 71510468a513..c52bf2a6b68c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts @@ -30,7 +30,7 @@ sentryTest( expect(requestCount).toBe(0); expect(consoleMessages).toEqual([ - 'You are using new BrowserTracing() even though this bundle does not include tracing.', + 'You are using browserTracingIntegration() even though this bundle does not include tracing.', ]); }, ); 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 8ac3d814b2bd..f2921f78c479 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 @@ -8,7 +8,6 @@ Sentry.init({ integrations: [Sentry.browserTracingIntegration()], environment: 'production', tracesSampleRate: 1, - debug: true, }); Sentry.setUser({ id: 'user123' }); 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 51f27f9122df..79b4b0c89555 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 @@ -8,7 +8,6 @@ Sentry.init({ tracePropagationTargets: [/.*/], environment: 'production', tracesSampleRate: 1, - debug: true, }); const scope = Sentry.getCurrentScope(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts index 23cd29099a0f..a2801f4e4016 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts @@ -29,9 +29,9 @@ sentryTest( expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.lcp?.value).toBeDefined(); - expect(eventData.tags?.['lcp.element']).toBe('body > img'); - expect(eventData.tags?.['lcp.size']).toBe(107400); - expect(eventData.tags?.['lcp.url']).toBe('https://example.com/path/to/image.png'); + expect(eventData.contexts?.trace?.data?.['lcp.element']).toBe('body > img'); + expect(eventData.contexts?.trace?.data?.['lcp.size']).toBe(107400); + expect(eventData.contexts?.trace?.data?.['lcp.url']).toBe('https://example.com/path/to/image.png'); const lcp = await (await page.waitForFunction('window._LCP')).jsonValue(); const lcp2 = await (await page.waitForFunction('window._LCP2')).jsonValue(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts index 0dee366c75f4..00fc906aa60e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts @@ -23,7 +23,7 @@ sentryTest('should capture a "GOOD" CLS vital with its source(s).', async ({ get expect(eventData.measurements?.cls?.value).toBeGreaterThan(0.03); expect(eventData.measurements?.cls?.value).toBeLessThan(0.07); - expect(eventData.tags?.['cls.source.1']).toBe('body > div#content > p#partial'); + expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p#partial'); }); sentryTest('should capture a "MEH" CLS vital with its source(s).', async ({ getLocalTestPath, page }) => { @@ -37,7 +37,7 @@ sentryTest('should capture a "MEH" CLS vital with its source(s).', async ({ getL expect(eventData.measurements?.cls?.value).toBeGreaterThan(0.18); expect(eventData.measurements?.cls?.value).toBeLessThan(0.23); - expect(eventData.tags?.['cls.source.1']).toBe('body > div#content > p'); + expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p'); }); sentryTest('should capture a "POOR" CLS vital with its source(s).', async ({ getLocalTestPath, page }) => { @@ -50,5 +50,5 @@ sentryTest('should capture a "POOR" CLS vital with its source(s).', async ({ get // Flakey value dependent on timings -> we check for a range expect(eventData.measurements?.cls?.value).toBeGreaterThan(0.34); expect(eventData.measurements?.cls?.value).toBeLessThan(0.36); - expect(eventData.tags?.['cls.source.1']).toBe('body > div#content > p'); + expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p'); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts index cbe60ae1ea62..2cfcbe58806e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts @@ -10,9 +10,10 @@ sentryTest('should capture a LCP vital with element details.', async ({ browserN sentryTest.skip(); } - await page.route('**/path/to/image.png', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/sentry-logo-600x179.png` }), - ); + page.route('**', route => route.continue()); + page.route('**/path/to/image.png', async (route: Route) => { + return route.fulfill({ path: `${__dirname}/assets/sentry-logo-600x179.png` }); + }); const url = await getLocalTestPath({ testDir: __dirname }); const [eventData] = await Promise.all([ @@ -24,7 +25,7 @@ sentryTest('should capture a LCP vital with element details.', async ({ browserN expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.lcp?.value).toBeDefined(); - expect(eventData.tags?.['lcp.element']).toBe('body > img'); - expect(eventData.tags?.['lcp.size']).toBe(107400); - expect(eventData.tags?.['lcp.url']).toBe('https://example.com/path/to/image.png'); + expect(eventData.contexts?.trace?.data?.['lcp.element']).toBe('body > img'); + expect(eventData.contexts?.trace?.data?.['lcp.size']).toBe(107400); + expect(eventData.contexts?.trace?.data?.['lcp.url']).toBe('https://example.com/path/to/image.png'); }); diff --git a/dev-packages/browser-integration-tests/suites/transport/offline/queued/test.ts b/dev-packages/browser-integration-tests/suites/transport/offline/queued/test.ts index 61020eceb79b..2ca225ee069a 100644 --- a/dev-packages/browser-integration-tests/suites/transport/offline/queued/test.ts +++ b/dev-packages/browser-integration-tests/suites/transport/offline/queued/test.ts @@ -10,7 +10,7 @@ function delay(ms: number) { sentryTest('should queue and retry events when they fail to send', async ({ getLocalTestPath, page }) => { // makeBrowserOfflineTransport is not included in any CDN bundles - if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) { + if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle')) { sentryTest.skip(); } diff --git a/dev-packages/browser-integration-tests/tsconfig.json b/dev-packages/browser-integration-tests/tsconfig.json index 6c252887a736..2587befcce14 100644 --- a/dev-packages/browser-integration-tests/tsconfig.json +++ b/dev-packages/browser-integration-tests/tsconfig.json @@ -5,7 +5,8 @@ "lib": ["dom", "es2019"], "moduleResolution": "node", "noEmit": true, - "strict": true + "strict": true, + "allowSyntheticDefaultImports": true }, "include": ["**/*.ts"], "exclude": ["node_modules"] diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index 738771aedced..f3875c561e74 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -47,38 +47,32 @@ const BUNDLE_PATHS: Record> = { browser: { cjs: 'build/npm/cjs/index.js', esm: 'build/npm/esm/index.js', - bundle_es5: 'build/bundles/bundle.es5.js', - bundle_es5_min: 'build/bundles/bundle.es5.min.js', - bundle_es6: 'build/bundles/bundle.js', - bundle_es6_min: 'build/bundles/bundle.min.js', - bundle_replay_es6: 'build/bundles/bundle.replay.js', - bundle_replay_es6_min: 'build/bundles/bundle.replay.min.js', - bundle_tracing_es5: 'build/bundles/bundle.tracing.es5.js', - bundle_tracing_es5_min: 'build/bundles/bundle.tracing.es5.min.js', - bundle_tracing_es6: 'build/bundles/bundle.tracing.js', - bundle_tracing_es6_min: 'build/bundles/bundle.tracing.min.js', - bundle_tracing_replay_es6: 'build/bundles/bundle.tracing.replay.js', - bundle_tracing_replay_es6_min: 'build/bundles/bundle.tracing.replay.min.js', - loader_base: 'build/bundles/bundle.es5.min.js', - loader_eager: 'build/bundles/bundle.es5.min.js', - loader_debug: 'build/bundles/bundle.es5.debug.min.js', - loader_tracing: 'build/bundles/bundle.tracing.es5.min.js', + bundle: 'build/bundles/bundle.js', + bundle_min: 'build/bundles/bundle.min.js', + bundle_replay: 'build/bundles/bundle.replay.js', + bundle_replay_min: 'build/bundles/bundle.replay.min.js', + bundle_tracing: 'build/bundles/bundle.tracing.js', + bundle_tracing_min: 'build/bundles/bundle.tracing.min.js', + bundle_tracing_replay: 'build/bundles/bundle.tracing.replay.js', + bundle_tracing_replay_min: 'build/bundles/bundle.tracing.replay.min.js', + loader_base: 'build/bundles/bundle.min.js', + loader_eager: 'build/bundles/bundle.min.js', + loader_debug: 'build/bundles/bundle.debug.min.js', + loader_tracing: 'build/bundles/bundle.tracing.min.js', loader_replay: 'build/bundles/bundle.replay.min.js', loader_tracing_replay: 'build/bundles/bundle.tracing.replay.debug.min.js', }, integrations: { cjs: 'build/npm/cjs/index.js', esm: 'build/npm/esm/index.js', - bundle_es5: 'build/bundles/[INTEGRATION_NAME].es5.js', - bundle_es5_min: 'build/bundles/[INTEGRATION_NAME].es5.min.js', - bundle_es6: 'build/bundles/[INTEGRATION_NAME].js', - bundle_es6_min: 'build/bundles/[INTEGRATION_NAME].min.js', + bundle: 'build/bundles/[INTEGRATION_NAME].js', + bundle_min: 'build/bundles/[INTEGRATION_NAME].min.js', }, wasm: { cjs: 'build/npm/cjs/index.js', esm: 'build/npm/esm/index.js', - bundle_es6: 'build/bundles/wasm.js', - bundle_es6_min: 'build/bundles/wasm.min.js', + bundle: 'build/bundles/wasm.js', + bundle_min: 'build/bundles/wasm.min.js', }, }; @@ -229,7 +223,7 @@ class SentryScenarioGenerationPlugin { }); } - // Convert e.g. bundle_tracing_es5_min to bundle_es5_min + // Convert e.g. bundle_tracing_min to bundle_min const integrationBundleKey = bundleKey .replace('loader_', 'bundle_') .replace('_replay', '') diff --git a/dev-packages/browser-integration-tests/utils/wasmHelpers.ts b/dev-packages/browser-integration-tests/utils/wasmHelpers.ts index ca93148e690c..cb8cf1bcb4b3 100644 --- a/dev-packages/browser-integration-tests/utils/wasmHelpers.ts +++ b/dev-packages/browser-integration-tests/utils/wasmHelpers.ts @@ -1,7 +1,7 @@ /** * We can only test WASM tests in certain bundles/packages: * - NPM (ESM, CJS) - * - ES6 CDN bundles + * - CDN bundles * - On browsers other than WebKit * * @returns `true` if we should skip the replay test @@ -11,5 +11,5 @@ export function shouldSkipWASMTests(browser: string): boolean { return true; } const bundle = process.env.PW_BUNDLE as string | undefined; - return bundle != null && bundle.includes('es5'); + return bundle != null; } diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.routes.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.routes.ts index d4d9a04f6c99..24bf8b769051 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.routes.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.routes.ts @@ -1,4 +1,7 @@ import { Routes } from '@angular/router'; +import { cancelGuard } from './cancel-guard.guard'; +import { CancelComponent } from './cancel/cancel.components'; +import { ComponentTrackingComponent } from './component-tracking/component-tracking.components'; import { HomeComponent } from './home/home.component'; import { UserComponent } from './user/user.component'; @@ -11,6 +14,15 @@ export const routes: Routes = [ path: 'home', component: HomeComponent, }, + { + path: 'cancel', + component: CancelComponent, + canActivate: [cancelGuard], + }, + { + path: 'component-tracking', + component: ComponentTrackingComponent, + }, { path: 'redirect1', redirectTo: '/redirect2', diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/cancel-guard.guard.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/cancel-guard.guard.ts new file mode 100644 index 000000000000..16ec4a2ab164 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/cancel-guard.guard.ts @@ -0,0 +1,5 @@ +import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; + +export const cancelGuard: CanActivateFn = (_next: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { + return false; +}; diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/cancel/cancel.components.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/cancel/cancel.components.ts new file mode 100644 index 000000000000..b6ee1876e035 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/cancel/cancel.components.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-cancel', + standalone: true, + template: `
`, +}) +export class CancelComponent {} diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts new file mode 100644 index 000000000000..e9aea4ccd68a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts @@ -0,0 +1,18 @@ +import { AfterViewInit, Component, OnInit } from '@angular/core'; +import { TraceClass, TraceMethod, TraceModule } from '@sentry/angular-ivy'; +import { SampleComponent } from '../sample-component/sample-component.components'; + +@Component({ + selector: 'app-cancel', + standalone: true, + imports: [TraceModule, SampleComponent], + template: ``, +}) +@TraceClass({ name: 'ComponentTrackingComponent' }) +export class ComponentTrackingComponent implements OnInit, AfterViewInit { + @TraceMethod({ name: 'ngOnInit' }) + ngOnInit() {} + + @TraceMethod() + ngAfterViewInit() {} +} diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/home/home.component.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/home/home.component.ts index 9179f5d79638..298d7f7d54cd 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/src/app/home/home.component.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/home/home.component.ts @@ -11,6 +11,9 @@ import { RouterLink } from '@angular/router'; diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/sample-component/sample-component.components.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/sample-component/sample-component.components.ts new file mode 100644 index 000000000000..bd331a9dbff0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/sample-component/sample-component.components.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-sample-component', + standalone: true, + template: `
`, +}) +export class SampleComponent implements OnInit { + ngOnInit() { + console.log('SampleComponent'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/user/user.component.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/user/user.component.ts index 087a33a4a2f1..db02568d395f 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/src/app/user/user.component.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/user/user.component.ts @@ -8,7 +8,8 @@ import { Observable, map } from 'rxjs'; standalone: true, imports: [AsyncPipe], template: ` -

Hello User {{ userId$ | async }}

+

Hello User {{ userId$ | async }}

+ `, }) export class UserComponent { @@ -17,4 +18,8 @@ export class UserComponent { constructor(private route: ActivatedRoute) { this.userId$ = this.route.paramMap.pipe(map(params => params.get('id') || 'UNKNOWN USER')); } + + throwError() { + throw new Error('Error thrown from user page'); + } } diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts index e4d687ca3199..4666893b1882 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '../event-proxy-server'; test('sends an error', async ({ page }) => { const errorPromise = waitForError('angular-17', async errorEvent => { @@ -25,5 +25,41 @@ test('sends an error', async ({ page }) => { }, ], }, + transaction: '/home/', + }); +}); + +test('assigns the correct transaction value after a navigation', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const errorPromise = waitForError('angular-17', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + await page.waitForTimeout(5000); + + await page.locator('#navLink').click(); + + const [_, error] = await Promise.all([page.locator('#userErrorBtn').click(), errorPromise]); + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from user page', + mechanism: { + type: 'angular', + handled: false, + }, + }, + ], + }, + transaction: '/users/:id/', }); }); diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts index 9b5f1b08e9d2..9fc4b74e779f 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { waitForTransaction } from '../event-proxy-server'; test('sends a pageload transaction with a parameterized URL', async ({ page }) => { @@ -126,3 +127,187 @@ test('groups redirects within one navigation root span', async ({ page }) => { expect(routingSpan).toBeDefined(); expect(routingSpan?.description).toBe('/redirect1'); }); + +test.describe('finish routing span', () => { + test('finishes routing span on navigation cancel', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#cancelLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: '/cancel', + transaction_info: { + source: 'url', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe('/cancel'); + }); + + test('finishes routing span on navigation error', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#nonExistentLink').click(), navigationTxnPromise]); + + const nonExistentRoute = '/non-existent'; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: nonExistentRoute, + transaction_info: { + source: 'url', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe(nonExistentRoute); + }); +}); + +test.describe('TraceDirective', () => { + test('creates a child tracingSpan with component name as span name on ngOnInit', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const traceDirectiveSpan = navigationTxn.spans?.find( + span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive', + ); + + expect(traceDirectiveSpan).toBeDefined(); + expect(traceDirectiveSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); +}); + +test.describe('TraceClass Decorator', () => { + test('adds init span for decorated class', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const classDecoratorSpan = navigationTxn.spans?.find( + span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_class_decorator', + ); + + expect(classDecoratorSpan).toBeDefined(); + expect(classDecoratorSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', + }, + description: '', + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_class_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); +}); + +test.describe('TraceMethod Decorator', () => { + test('adds name to span description of decorated method `ngOnInit`', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const ngInitSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.ngOnInit'); + + expect(ngInitSpan).toBeDefined(); + expect(ngInitSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.ngOnInit', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + }, + description: '', + op: 'ui.angular.ngOnInit', + origin: 'auto.ui.angular.trace_method_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); + + test('adds fallback name to span description of decorated method `ngAfterViewInit`', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const ngAfterViewInitSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.ngAfterViewInit'); + + expect(ngAfterViewInitSpan).toBeDefined(); + expect(ngAfterViewInitSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.ngAfterViewInit', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + }, + description: '', + op: 'ui.angular.ngAfterViewInit', + origin: 'auto.ui.angular.trace_method_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/create-next-app/tsconfig.json index 3ff0501fdb85..1fd2e9a8d510 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/create-next-app/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/dev-packages/e2e-tests/test-applications/create-react-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/create-react-app/tsconfig.json index 4bd4dd6a0417..bd19e4f07fc7 100644 --- a/dev-packages/e2e-tests/test-applications/create-react-app/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/create-react-app/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/README.md b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/README.md index ec619a8eb455..31400e85106f 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/README.md +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/README.md @@ -1,6 +1,7 @@ # Welcome to Remix + Vite! -📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features. +📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) +for details on supported features. ## Development @@ -28,7 +29,8 @@ Now you'll need to pick a host to deploy it to. ### DIY -If you're familiar with deploying Express applications you should be right at home. Just make sure to deploy the output of `npm run build` +If you're familiar with deploying Express applications you should be right at home. Just make sure to deploy the output +of `npm run build` - `build/server` - `build/client` diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.client.tsx index d71aaa5cd286..4eb7e3d3553f 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.client.tsx @@ -12,13 +12,14 @@ Sentry.init({ useLocation, useMatches, }), - new Sentry.Replay(), + Sentry.replayIntegration(), ], // Performance Monitoring tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! // Session Replay replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. + tunnel: 'http://localhost:3031/', // proxy server }); Sentry.addEventProcessor(event => { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.server.tsx index c3deb6369af3..5e0608ff5749 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.server.tsx @@ -1,10 +1,18 @@ +import * as Sentry from '@sentry/remix'; + +Sentry.init({ + tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + tunnel: 'http://localhost:3031/', // proxy server +}); + import { PassThrough } from 'node:stream'; import type { AppLoadContext, EntryContext } from '@remix-run/node'; import { createReadableStreamFromReadable } from '@remix-run/node'; import { installGlobals } from '@remix-run/node'; import { RemixServer } from '@remix-run/react'; -import * as Sentry from '@sentry/remix'; import * as isbotModule from 'isbot'; import { renderToPipeableStream } from 'react-dom/server'; @@ -12,13 +20,6 @@ installGlobals(); const ABORT_DELAY = 5_000; -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.E2E_TEST_DSN, - // Performance Monitoring - tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! -}); - export const handleError = Sentry.wrapRemixHandleError; export default function handleRequest( diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/routes/_index.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/routes/_index.tsx index 8907ef7816fd..b646c62ee4da 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/routes/_index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/routes/_index.tsx @@ -1,7 +1,13 @@ -import { Link } from '@remix-run/react'; +import { Link, useSearchParams } from '@remix-run/react'; import * as Sentry from '@sentry/remix'; export default function Index() { + const [searchParams] = useSearchParams(); + + if (searchParams.get('tag')) { + Sentry.setTag('sentry_test', searchParams.get('tag')); + } + return (
=18.0.0" diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/playwright.config.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/playwright.config.ts index 9f04a7ee7896..dd495b0c9f98 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/playwright.config.ts @@ -2,6 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; const port = 3030; +const eventProxyPort = 3031; /** * See https://playwright.dev/docs/test-configuration. @@ -34,6 +35,9 @@ const config: PlaywrightTestConfig = { /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', + + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${port}`, }, /* Configure projects for major browsers */ @@ -44,15 +48,19 @@ const config: PlaywrightTestConfig = { ...devices['Desktop Chrome'], }, }, - // For now we only test Chrome! ], /* Run your local dev server before starting the tests */ - webServer: { - // This test app is testing the Vite dev server, so we need to run it before the tests. - command: `PORT=${port} pnpm dev`, - port, - }, + webServer: [ + { + command: 'pnpm ts-node --project="tsconfig.event-proxy-server.json" ./start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: `PORT=${port} pnpm dev`, + port: port, + }, + ], }; export default config; diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts new file mode 100644 index 000000000000..e56a52190e63 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts @@ -0,0 +1,5 @@ +import { startEventProxyServer } from './event-proxy-server'; +startEventProxyServer({ + port: 3031, + proxyServerName: 'create-remix-app-express-vite-dev', +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts new file mode 100644 index 000000000000..09cee3ca79bc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test'; +import { uuid4 } from '@sentry/utils'; + +import { waitForTransaction } from '../event-proxy-server'; + +test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { + // We use this to identify the transactions + const testTag = uuid4(); + + const httpServerTransactionPromise = waitForTransaction('create-remix-app-express-vite-dev', transactionEvent => { + return ( + transactionEvent.type === 'transaction' && + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.tags?.['sentry_test'] === testTag + ); + }); + + const pageLoadTransactionPromise = waitForTransaction('create-remix-app-express-vite-dev', transactionEvent => { + return ( + transactionEvent.type === 'transaction' && + transactionEvent.contexts?.trace?.op === 'pageload' && + transactionEvent.tags?.['sentry_test'] === testTag + ); + }); + + page.goto(`/?tag=${testTag}`); + + const pageloadTransaction = await pageLoadTransactionPromise; + const httpServerTransaction = await httpServerTransactionPromise; + + expect(pageloadTransaction).toBeDefined(); + expect(httpServerTransaction).toBeDefined(); + + const httpServerTraceId = httpServerTransaction.contexts?.trace?.trace_id; + const httpServerSpanId = httpServerTransaction.contexts?.trace?.span_id; + + const pageLoadTraceId = pageloadTransaction.contexts?.trace?.trace_id; + const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id; + const pageLoadParentSpanId = pageloadTransaction.contexts?.trace?.parent_span_id; + + expect(httpServerTransaction.transaction).toBe('routes/_index'); + expect(pageloadTransaction.transaction).toBe('routes/_index'); + + expect(httpServerTraceId).toBeDefined(); + expect(httpServerSpanId).toBeDefined(); + + expect(pageLoadTraceId).toEqual(httpServerTraceId); + expect(pageLoadParentSpanId).toEqual(httpServerSpanId); + expect(pageLoadSpanId).not.toEqual(httpServerSpanId); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tsconfig.event-proxy-server.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tsconfig.event-proxy-server.json new file mode 100644 index 000000000000..bd49b1f0c16f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tsconfig.event-proxy-server.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "allowJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "allowImportingTsExtensions": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tsconfig.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tsconfig.json index 77291a910914..0a6c9071cb90 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tsconfig.json @@ -1,5 +1,6 @@ { "include": ["env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["event-proxy-server.ts", "start-event-proxy.ts"], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], "isolatedModules": true, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx index 1f1a48d28fbd..b3b5db3d9b3d 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx @@ -18,13 +18,14 @@ Sentry.init({ useLocation, useMatches, }), - new Sentry.Replay(), + Sentry.replayIntegration(), ], // Performance Monitoring tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! // Session Replay replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. + tunnel: 'http://localhost:3031/', // proxy server }); Sentry.addEventProcessor(event => { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx index d8e63095fa43..0f4ea48a99e9 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx @@ -1,3 +1,12 @@ +import * as Sentry from '@sentry/remix'; + +Sentry.init({ + tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + tunnel: 'http://localhost:3031/', // proxy server +}); + /** * By default, Remix will handle generating the HTTP Response for you. * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ @@ -10,7 +19,6 @@ import type { AppLoadContext, EntryContext } from '@remix-run/node'; import { createReadableStreamFromReadable } from '@remix-run/node'; import { installGlobals } from '@remix-run/node'; import { RemixServer } from '@remix-run/react'; -import * as Sentry from '@sentry/remix'; import isbot from 'isbot'; import { renderToPipeableStream } from 'react-dom/server'; @@ -18,13 +26,6 @@ installGlobals(); const ABORT_DELAY = 5_000; -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.E2E_TEST_DSN, - // Performance Monitoring - tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! -}); - export const handleError = Sentry.wrapRemixHandleError; export default function handleRequest( diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/routes/_index.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/routes/_index.tsx index 8907ef7816fd..b646c62ee4da 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/routes/_index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/routes/_index.tsx @@ -1,7 +1,13 @@ -import { Link } from '@remix-run/react'; +import { Link, useSearchParams } from '@remix-run/react'; import * as Sentry from '@sentry/remix'; export default function Index() { + const [searchParams] = useSearchParams(); + + if (searchParams.get('tag')) { + Sentry.setTag('sentry_test', searchParams.get('tag')); + } + return (
{ + const eventCallbackListeners: Set<(data: string) => void> = new Set(); + + const proxyServer = http.createServer((proxyRequest, proxyResponse) => { + const proxyRequestChunks: Uint8Array[] = []; + + proxyRequest.addListener('data', (chunk: Buffer) => { + proxyRequestChunks.push(chunk); + }); + + proxyRequest.addListener('error', err => { + throw err; + }); + + proxyRequest.addListener('end', () => { + const proxyRequestBody = + proxyRequest.headers['content-encoding'] === 'gzip' + ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() + : Buffer.concat(proxyRequestChunks).toString(); + + let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); + + if (!envelopeHeader.dsn) { + throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); + } + + const { origin, pathname, host } = new URL(envelopeHeader.dsn); + + const projectId = pathname.substring(1); + const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; + + proxyRequest.headers.host = host; + + const sentryResponseChunks: Uint8Array[] = []; + + const sentryRequest = https.request( + sentryIngestUrl, + { headers: proxyRequest.headers, method: proxyRequest.method }, + sentryResponse => { + sentryResponse.addListener('data', (chunk: Buffer) => { + proxyResponse.write(chunk, 'binary'); + sentryResponseChunks.push(chunk); + }); + + sentryResponse.addListener('end', () => { + eventCallbackListeners.forEach(listener => { + const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); + + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody, + sentryResponseStatusCode: sentryResponse.statusCode, + }; + + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + proxyResponse.end(); + }); + + sentryResponse.addListener('error', err => { + throw err; + }); + + proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); + }, + ); + + sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); + sentryRequest.end(); + }); + }); + + const proxyServerStartupPromise = new Promise(resolve => { + proxyServer.listen(options.port, () => { + resolve(); + }); + }); + + const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { + eventCallbackResponse.statusCode = 200; + eventCallbackResponse.setHeader('connection', 'keep-alive'); + + const callbackListener = (data: string): void => { + eventCallbackResponse.write(data.concat('\n'), 'utf8'); + }; + + eventCallbackListeners.add(callbackListener); + + eventCallbackRequest.on('close', () => { + eventCallbackListeners.delete(callbackListener); + }); + + eventCallbackRequest.on('error', () => { + eventCallbackListeners.delete(callbackListener); + }); + }); + + const eventCallbackServerStartupPromise = new Promise(resolve => { + eventCallbackServer.listen(0, () => { + const port = String((eventCallbackServer.address() as AddressInfo).port); + void registerCallbackServerPort(options.proxyServerName, port).then(resolve); + }); + }); + + await eventCallbackServerStartupPromise; + await proxyServerStartupPromise; + return; +} + +export async function waitForRequest( + proxyServerName: string, + callback: (eventData: SentryRequestCallbackData) => Promise | boolean, +): Promise { + const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); + + return new Promise((resolve, reject) => { + const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { + let eventContents = ''; + + response.on('error', err => { + reject(err); + }); + + response.on('data', (chunk: Buffer) => { + const chunkString = chunk.toString('utf8'); + chunkString.split('').forEach(char => { + if (char === '\n') { + const eventCallbackData: SentryRequestCallbackData = JSON.parse( + Buffer.from(eventContents, 'base64').toString('utf8'), + ); + const callbackResult = callback(eventCallbackData); + if (typeof callbackResult !== 'boolean') { + callbackResult.then( + match => { + if (match) { + response.destroy(); + resolve(eventCallbackData); + } + }, + err => { + throw err; + }, + ); + } else if (callbackResult) { + response.destroy(); + resolve(eventCallbackData); + } + eventContents = ''; + } else { + eventContents = eventContents.concat(char); + } + }); + }); + }); + + request.end(); + }); +} + +export function waitForEnvelopeItem( + proxyServerName: string, + callback: (envelopeItem: EnvelopeItem) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForRequest(proxyServerName, async eventData => { + const envelopeItems = eventData.envelope[1]; + for (const envelopeItem of envelopeItems) { + if (await callback(envelopeItem)) { + resolve(envelopeItem); + return true; + } + } + return false; + }).catch(reject); + }); +} + +export function waitForError( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +const TEMP_FILE_PREFIX = 'event-proxy-server-'; + +async function registerCallbackServerPort(serverName: string, port: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + await writeFile(tmpFilePath, port, { encoding: 'utf8' }); +} + +function retrieveCallbackServerPort(serverName: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + return readFile(tmpFilePath, 'utf8'); +} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json index 71ff5ff39803..646bc5f21e25 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json @@ -24,10 +24,13 @@ "@playwright/test": "^1.36.2", "@remix-run/dev": "2.7.2", "@remix-run/eslint-config": "2.7.2", + "@sentry/types": "latest || *", + "@sentry/utils": "latest || *", "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", "eslint": "^8.38.0", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "ts-node": "10.9.1" }, "engines": { "node": ">=18.0.0" diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/playwright.config.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/playwright.config.ts index 79efcbc22c1a..429baa2db33f 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/playwright.config.ts @@ -2,6 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; const port = 3030; +const eventProxyPort = 3031; /** * See https://playwright.dev/docs/test-configuration. @@ -34,6 +35,9 @@ const config: PlaywrightTestConfig = { /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', + + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${port}`, }, /* Configure projects for major browsers */ @@ -44,14 +48,19 @@ const config: PlaywrightTestConfig = { ...devices['Desktop Chrome'], }, }, - // For now we only test Chrome! ], /* Run your local dev server before starting the tests */ - webServer: { - command: `PORT=${port} pnpm start`, - port, - }, + webServer: [ + { + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: `PORT=${port} pnpm start`, + port: port, + }, + ], }; export default config; diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts new file mode 100644 index 000000000000..cc810192de58 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts @@ -0,0 +1,5 @@ +import { startEventProxyServer } from './event-proxy-server'; +startEventProxyServer({ + port: 3031, + proxyServerName: 'create-remix-app-v2', +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts new file mode 100644 index 000000000000..992a315af3d3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test'; +import { uuid4 } from '@sentry/utils'; + +import { waitForTransaction } from '../event-proxy-server'; + +test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { + // We use this to identify the transactions + const testTag = uuid4(); + + const httpServerTransactionPromise = waitForTransaction('create-remix-app-v2', transactionEvent => { + return ( + transactionEvent.type === 'transaction' && + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.tags?.['sentry_test'] === testTag + ); + }); + + const pageLoadTransactionPromise = waitForTransaction('create-remix-app-v2', transactionEvent => { + return ( + transactionEvent.type === 'transaction' && + transactionEvent.contexts?.trace?.op === 'pageload' && + transactionEvent.tags?.['sentry_test'] === testTag + ); + }); + + page.goto(`/?tag=${testTag}`); + + const pageloadTransaction = await pageLoadTransactionPromise; + const httpServerTransaction = await httpServerTransactionPromise; + + expect(pageloadTransaction).toBeDefined(); + expect(httpServerTransaction).toBeDefined(); + + const httpServerTraceId = httpServerTransaction.contexts?.trace?.trace_id; + const httpServerSpanId = httpServerTransaction.contexts?.trace?.span_id; + + const pageLoadTraceId = pageloadTransaction.contexts?.trace?.trace_id; + const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id; + const pageLoadParentSpanId = pageloadTransaction.contexts?.trace?.parent_span_id; + + expect(httpServerTransaction.transaction).toBe('routes/_index'); + expect(pageloadTransaction.transaction).toBe('routes/_index'); + + expect(httpServerTraceId).toBeDefined(); + expect(httpServerSpanId).toBeDefined(); + + expect(pageLoadTraceId).toEqual(httpServerTraceId); + expect(pageLoadParentSpanId).toEqual(httpServerSpanId); + expect(pageLoadSpanId).not.toEqual(httpServerSpanId); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx index d5a8978376a4..93eab0f819fb 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx @@ -12,12 +12,13 @@ import { hydrateRoot } from 'react-dom/client'; Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: window.ENV.SENTRY_DSN, - integrations: [Sentry.browserTracingIntegration({ useEffect, useMatches, useLocation }), new Sentry.Replay()], + integrations: [Sentry.browserTracingIntegration({ useEffect, useMatches, useLocation }), Sentry.replayIntegration()], // Performance Monitoring tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! // Session Replay replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. + tunnel: 'http://localhost:3031/', // proxy server }); Sentry.addEventProcessor(event => { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx index c2de73cdba63..b0f1c5d19f09 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx @@ -1,3 +1,12 @@ +import * as Sentry from '@sentry/remix'; + +Sentry.init({ + tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + tunnel: 'http://localhost:3031/', // proxy server +}); + /** * By default, Remix will handle generating the HTTP Response for you. * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ @@ -9,19 +18,11 @@ import { PassThrough } from 'node:stream'; import type { AppLoadContext, EntryContext } from '@remix-run/node'; import { Response } from '@remix-run/node'; import { RemixServer } from '@remix-run/react'; -import * as Sentry from '@sentry/remix'; import isbot from 'isbot'; import { renderToPipeableStream } from 'react-dom/server'; const ABORT_DELAY = 5_000; -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.E2E_TEST_DSN, - // Performance Monitoring - tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! -}); - export const handleError = Sentry.wrapRemixHandleError; export default function handleRequest( diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx index 8907ef7816fd..b646c62ee4da 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx @@ -1,7 +1,13 @@ -import { Link } from '@remix-run/react'; +import { Link, useSearchParams } from '@remix-run/react'; import * as Sentry from '@sentry/remix'; export default function Index() { + const [searchParams] = useSearchParams(); + + if (searchParams.get('tag')) { + Sentry.setTag('sentry_test', searchParams.get('tag')); + } + return (
{ + const eventCallbackListeners: Set<(data: string) => void> = new Set(); + + const proxyServer = http.createServer((proxyRequest, proxyResponse) => { + const proxyRequestChunks: Uint8Array[] = []; + + proxyRequest.addListener('data', (chunk: Buffer) => { + proxyRequestChunks.push(chunk); + }); + + proxyRequest.addListener('error', err => { + throw err; + }); + + proxyRequest.addListener('end', () => { + const proxyRequestBody = + proxyRequest.headers['content-encoding'] === 'gzip' + ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() + : Buffer.concat(proxyRequestChunks).toString(); + + let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); + + if (!envelopeHeader.dsn) { + throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); + } + + const { origin, pathname, host } = new URL(envelopeHeader.dsn); + + const projectId = pathname.substring(1); + const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; + + proxyRequest.headers.host = host; + + const sentryResponseChunks: Uint8Array[] = []; + + const sentryRequest = https.request( + sentryIngestUrl, + { headers: proxyRequest.headers, method: proxyRequest.method }, + sentryResponse => { + sentryResponse.addListener('data', (chunk: Buffer) => { + proxyResponse.write(chunk, 'binary'); + sentryResponseChunks.push(chunk); + }); + + sentryResponse.addListener('end', () => { + eventCallbackListeners.forEach(listener => { + const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); + + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody, + sentryResponseStatusCode: sentryResponse.statusCode, + }; + + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + proxyResponse.end(); + }); + + sentryResponse.addListener('error', err => { + throw err; + }); + + proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); + }, + ); + + sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); + sentryRequest.end(); + }); + }); + + const proxyServerStartupPromise = new Promise(resolve => { + proxyServer.listen(options.port, () => { + resolve(); + }); + }); + + const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { + eventCallbackResponse.statusCode = 200; + eventCallbackResponse.setHeader('connection', 'keep-alive'); + + const callbackListener = (data: string): void => { + eventCallbackResponse.write(data.concat('\n'), 'utf8'); + }; + + eventCallbackListeners.add(callbackListener); + + eventCallbackRequest.on('close', () => { + eventCallbackListeners.delete(callbackListener); + }); + + eventCallbackRequest.on('error', () => { + eventCallbackListeners.delete(callbackListener); + }); + }); + + const eventCallbackServerStartupPromise = new Promise(resolve => { + eventCallbackServer.listen(0, () => { + const port = String((eventCallbackServer.address() as AddressInfo).port); + void registerCallbackServerPort(options.proxyServerName, port).then(resolve); + }); + }); + + await eventCallbackServerStartupPromise; + await proxyServerStartupPromise; + return; +} + +export async function waitForRequest( + proxyServerName: string, + callback: (eventData: SentryRequestCallbackData) => Promise | boolean, +): Promise { + const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); + + return new Promise((resolve, reject) => { + const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { + let eventContents = ''; + + response.on('error', err => { + reject(err); + }); + + response.on('data', (chunk: Buffer) => { + const chunkString = chunk.toString('utf8'); + chunkString.split('').forEach(char => { + if (char === '\n') { + const eventCallbackData: SentryRequestCallbackData = JSON.parse( + Buffer.from(eventContents, 'base64').toString('utf8'), + ); + const callbackResult = callback(eventCallbackData); + if (typeof callbackResult !== 'boolean') { + callbackResult.then( + match => { + if (match) { + response.destroy(); + resolve(eventCallbackData); + } + }, + err => { + throw err; + }, + ); + } else if (callbackResult) { + response.destroy(); + resolve(eventCallbackData); + } + eventContents = ''; + } else { + eventContents = eventContents.concat(char); + } + }); + }); + }); + + request.end(); + }); +} + +export function waitForEnvelopeItem( + proxyServerName: string, + callback: (envelopeItem: EnvelopeItem) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForRequest(proxyServerName, async eventData => { + const envelopeItems = eventData.envelope[1]; + for (const envelopeItem of envelopeItems) { + if (await callback(envelopeItem)) { + resolve(envelopeItem); + return true; + } + } + return false; + }).catch(reject); + }); +} + +export function waitForError( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +const TEMP_FILE_PREFIX = 'event-proxy-server-'; + +async function registerCallbackServerPort(serverName: string, port: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + await writeFile(tmpFilePath, port, { encoding: 'utf8' }); +} + +function retrieveCallbackServerPort(serverName: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + return readFile(tmpFilePath, 'utf8'); +} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json index 5ff93d15a484..365fd9fb0bac 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json @@ -26,11 +26,14 @@ "@remix-run/eslint-config": "^1.19.3", "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", + "@sentry/types": "latest || *", + "@sentry/utils": "latest || *", "eslint": "^8.38.0", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "ts-node": "10.9.1" }, "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/playwright.config.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/playwright.config.ts index 785ca43321a4..429baa2db33f 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/playwright.config.ts @@ -2,6 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; const port = 3030; +const eventProxyPort = 3031; /** * See https://playwright.dev/docs/test-configuration. @@ -34,6 +35,9 @@ const config: PlaywrightTestConfig = { /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', + + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${port}`, }, /* Configure projects for major browsers */ @@ -44,26 +48,19 @@ const config: PlaywrightTestConfig = { ...devices['Desktop Chrome'], }, }, - // For now we only test Chrome! - // { - // name: 'firefox', - // use: { - // ...devices['Desktop Firefox'], - // }, - // }, - // { - // name: 'webkit', - // use: { - // ...devices['Desktop Safari'], - // }, - // }, ], /* Run your local dev server before starting the tests */ - webServer: { - command: `PORT=${port} pnpm start`, - port, - }, + webServer: [ + { + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: `PORT=${port} pnpm start`, + port: port, + }, + ], }; export default config; diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts new file mode 100644 index 000000000000..93755c9d232e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts @@ -0,0 +1,5 @@ +import { startEventProxyServer } from './event-proxy-server'; +startEventProxyServer({ + port: 3031, + proxyServerName: 'create-remix-app', +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts new file mode 100644 index 000000000000..d0d737e44a69 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test'; +import { uuid4 } from '@sentry/utils'; + +import { waitForTransaction } from '../event-proxy-server'; + +test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { + // We use this to identify the transactions + const testTag = uuid4(); + + const httpServerTransactionPromise = waitForTransaction('create-remix-app', transactionEvent => { + return ( + transactionEvent.type === 'transaction' && + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.tags?.['sentry_test'] === testTag + ); + }); + + const pageLoadTransactionPromise = waitForTransaction('create-remix-app', transactionEvent => { + return ( + transactionEvent.type === 'transaction' && + transactionEvent.contexts?.trace?.op === 'pageload' && + transactionEvent.tags?.['sentry_test'] === testTag + ); + }); + + page.goto(`/?tag=${testTag}`); + + const pageloadTransaction = await pageLoadTransactionPromise; + const httpServerTransaction = await httpServerTransactionPromise; + + expect(pageloadTransaction).toBeDefined(); + expect(httpServerTransaction).toBeDefined(); + + const httpServerTraceId = httpServerTransaction.contexts?.trace?.trace_id; + const httpServerSpanId = httpServerTransaction.contexts?.trace?.span_id; + + const pageLoadTraceId = pageloadTransaction.contexts?.trace?.trace_id; + const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id; + const pageLoadParentSpanId = pageloadTransaction.contexts?.trace?.parent_span_id; + + expect(httpServerTransaction.transaction).toBe('routes/_index'); + expect(pageloadTransaction.transaction).toBe('routes/_index'); + + expect(httpServerTraceId).toBeDefined(); + expect(httpServerSpanId).toBeDefined(); + + expect(pageLoadTraceId).toEqual(httpServerTraceId); + expect(pageLoadParentSpanId).toEqual(httpServerSpanId); + expect(pageLoadSpanId).not.toEqual(httpServerSpanId); +}); diff --git a/dev-packages/e2e-tests/test-applications/generic-ts3.8/tsconfig.json b/dev-packages/e2e-tests/test-applications/generic-ts3.8/tsconfig.json index 932aa58e0c9a..ef27756e97d9 100644 --- a/dev-packages/e2e-tests/test-applications/generic-ts3.8/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/generic-ts3.8/tsconfig.json @@ -1,11 +1,11 @@ { "include": ["index.ts"], "compilerOptions": { - "lib": ["es6", "DOM"], + "lib": ["es2017", "DOM"], "skipLibCheck": false, "noEmit": true, "types": [], - "target": "es6", + "target": "es2017", "moduleResolution": "node" } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json index b822f4316566..f2a1fb3d8a68 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -13,17 +13,17 @@ "test:assert": "pnpm test:prod && pnpm test:dev" }, "dependencies": { + "@playwright/test": "^1.27.1", "@sentry/nextjs": "latest || *", "@types/node": "18.11.17", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "14.0.4", + "next": "14.1.3", "react": "18.2.0", "react-dom": "18.2.0", - "typescript": "4.9.5", - "wait-port": "1.0.4", "ts-node": "10.9.1", - "@playwright/test": "^1.27.1" + "typescript": "4.9.5", + "wait-port": "1.0.4" }, "devDependencies": { "@sentry/types": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts index b5fe7ee67393..e52dde8db258 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts @@ -7,7 +7,7 @@ test('Should send a transaction event for a generateMetadata() function invokati const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions)' && - transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + (transactionEvent.extra?.route_data as any)?.searchParams?.metadataTitle === testTitle ); }); @@ -26,8 +26,8 @@ test('Should send a transaction and an error event for a faulty generateMetadata const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( - transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions)' && - transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + transactionEvent.transaction === 'Page.generateMetadata (/generation-functions)' && + (transactionEvent.extra?.route_data as any)?.searchParams?.metadataTitle === testTitle ); }); @@ -47,7 +47,7 @@ test('Should send a transaction event for a generateViewport() function invokati const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( transactionEvent?.transaction === 'Page.generateViewport (/generation-functions)' && - transactionEvent.contexts?.trace?.data?.['searchParams']?.['viewportThemeColor'] === testTitle + (transactionEvent.extra?.route_data as any)?.searchParams?.viewportThemeColor === testTitle ); }); @@ -64,7 +64,7 @@ test('Should send a transaction and an error event for a faulty generateViewport const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( transactionEvent?.transaction === 'Page.generateViewport (/generation-functions)' && - transactionEvent.contexts?.trace?.data?.['searchParams']?.['viewportThemeColor'] === testTitle + (transactionEvent.extra?.route_data as any)?.searchParams?.viewportThemeColor === testTitle ); }); @@ -86,7 +86,7 @@ test('Should send a transaction event with correct status for a generateMetadata const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions/with-redirect)' && - transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + (transactionEvent.extra?.route_data as any)?.searchParams?.metadataTitle === testTitle ); }); @@ -103,7 +103,7 @@ test('Should send a transaction event with correct status for a generateMetadata const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions/with-notfound)' && - transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + (transactionEvent.extra?.route_data as any)?.searchParams?.metadataTitle === testTitle ); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-14/tsconfig.json index 60825545944d..6b81123d463c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 421914877ce2..41f8d897d97b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -52,7 +52,8 @@ test('Should record exceptions and transactions for faulty route handlers', asyn expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-error'); - expect(routehandlerError.transaction).toBe('PUT /route-handlers/[param]/error'); + // TODO: Uncomment once we update the scope transaction name on the server side + // expect(routehandlerError.transaction).toBe('PUT /route-handlers/[param]/error'); }); test.describe('Edge runtime', () => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tsconfig.json index 60825545944d..6b81123d463c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json index 6d187f14c245..f8576bb04812 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json @@ -13,11 +13,13 @@ }, "dependencies": { "@sentry/node-experimental": "latest || *", + "@sentry/node": "latest || *", "@sentry/sveltekit": "latest || *", "@sentry/remix": "latest || *", "@sentry/astro": "latest || *", "@sentry/nextjs": "latest || *", - "@sentry/serverless": "latest || *", + "@sentry/aws-serverless": "latest || *", + "@sentry/google-cloud-serverless": "latest || *", "@sentry/bun": "latest || *", "@sentry/types": "latest || *", "@types/node": "18.15.1", diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index 5e7344588d56..c238cf326d68 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -1,78 +1,115 @@ import * as SentryAstro from '@sentry/astro'; import * as SentryBun from '@sentry/bun'; import * as SentryNextJs from '@sentry/nextjs'; -import * as SentryNode from '@sentry/node-experimental'; +import * as SentryNode from '@sentry/node'; +import * as SentryNodeExperimental from '@sentry/node-experimental'; import * as SentryRemix from '@sentry/remix'; -import * as SentryServerless from '@sentry/serverless'; import * as SentrySvelteKit from '@sentry/sveltekit'; +// Serverless SDKs are CJS only +const SentryAWS = require('@sentry/aws-serverless'); +const SentryGoogleCloud = require('@sentry/google-cloud-serverless'); + /* List of exports that are safe to ignore / we don't require in any depending package */ -const NODE_EXPORTS_IGNORE = [ +const NODE_EXPERIMENTAL_EXPORTS_IGNORE = [ 'default', // Probably generated by transpilation, no need to require it '__esModule', - // These Node exports were only made for type definition fixes (see #10339) - 'Undici', + // These are not re-exported where not needed 'Http', - 'DebugSession', - 'AnrIntegrationOptions', - 'LocalVariablesIntegrationOptions', + 'Undici', ]; +/* List of exports that are safe to ignore / we don't require in any depending package */ +const NODE_EXPORTS_IGNORE = [ + 'default', + // Probably generated by transpilation, no need to require it + '__esModule', +]; + +/* Sanitized list of node exports */ +const nodeExperimentalExports = Object.keys(SentryNodeExperimental).filter( + e => !NODE_EXPERIMENTAL_EXPORTS_IGNORE.includes(e), +); +const nodeExports = Object.keys(SentryNode).filter(e => !NODE_EXPORTS_IGNORE.includes(e)); + type Dependent = { package: string; exports: string[]; ignoreExports?: string[]; skip?: boolean; + compareWith: string[]; }; const DEPENDENTS: Dependent[] = [ { package: '@sentry/astro', + compareWith: nodeExports, exports: Object.keys(SentryAstro), + ignoreExports: [ + // Not needed for Astro + 'setupFastifyErrorHandler', + ], }, { package: '@sentry/bun', + compareWith: nodeExports, exports: Object.keys(SentryBun), ignoreExports: [ // not supported in bun: - 'Handlers', 'NodeClient', - 'hapiErrorPlugin', - 'makeNodeTransport', + // legacy, to be removed... + 'makeMain', ], }, { package: '@sentry/nextjs', + compareWith: nodeExperimentalExports, // Next.js doesn't require explicit exports, so we can just merge top level and `default` exports: // @ts-expect-error: `default` is not in the type definition but it's defined exports: Object.keys({ ...SentryNextJs, ...SentryNextJs.default }), }, { package: '@sentry/remix', + compareWith: nodeExports, exports: Object.keys(SentryRemix), }, { - package: '@sentry/serverless', - exports: Object.keys(SentryServerless), - ignoreExports: ['cron', 'hapiErrorPlugin'], + package: '@sentry/aws-serverless', + compareWith: nodeExports, + exports: Object.keys(SentryAWS), + ignoreExports: [ + // legacy, to be removed... + 'makeMain', + // Not needed for Serverless + 'setupFastifyErrorHandler', + ], + }, + { + package: '@sentry/google-cloud-serverless', + compareWith: nodeExports, + exports: Object.keys(SentryGoogleCloud), + ignoreExports: [ + // legacy, to be removed... + 'makeMain', + // Not needed for Serverless + 'setupFastifyErrorHandler', + ], }, { package: '@sentry/sveltekit', + compareWith: nodeExperimentalExports, exports: Object.keys(SentrySvelteKit), }, ]; -/* Sanitized list of node exports */ -const nodeExports = Object.keys(SentryNode).filter(e => !NODE_EXPORTS_IGNORE.includes(e)); - console.log('🔎 Checking for consistent exports of @sentry/node exports in depending packages'); const missingExports: Record = {}; const dependentsToCheck = DEPENDENTS.filter(d => !d.skip); -for (const nodeExport of nodeExports) { - for (const dependent of dependentsToCheck) { +for (const dependent of dependentsToCheck) { + for (const nodeExport of dependent.compareWith) { if (dependent.ignoreExports?.includes(nodeExport)) { continue; } diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json index fc22710d69dc..6f37f0817c4a 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "types": ["node"], "esModuleInterop": true, - "lib": ["ES6"], + "lib": ["es2017"], "strict": true, "outDir": "dist", "target": "ESNext", diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-express-app/tsconfig.json index decfcf7a0879..d46ae8103211 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-express-app/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "types": ["node"], "esModuleInterop": true, - "lib": ["ES6"], + "lib": ["es2017"], "strict": true, "outDir": "dist" }, diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/.gitignore b/dev-packages/e2e-tests/test-applications/node-fastify-app/.gitignore similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/.gitignore rename to dev-packages/e2e-tests/test-applications/node-fastify-app/.gitignore diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc b/dev-packages/e2e-tests/test-applications/node-fastify-app/.npmrc similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc rename to dev-packages/e2e-tests/test-applications/node-fastify-app/.npmrc diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts new file mode 100644 index 000000000000..d14ca5cb5e72 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts @@ -0,0 +1,253 @@ +import * as fs from 'fs'; +import * as http from 'http'; +import * as https from 'https'; +import type { AddressInfo } from 'net'; +import * as os from 'os'; +import * as path from 'path'; +import * as util from 'util'; +import * as zlib from 'zlib'; +import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import { parseEnvelope } from '@sentry/utils'; + +const readFile = util.promisify(fs.readFile); +const writeFile = util.promisify(fs.writeFile); + +interface EventProxyServerOptions { + /** Port to start the event proxy server at. */ + port: number; + /** The name for the proxy server used for referencing it with listener functions */ + proxyServerName: string; +} + +interface SentryRequestCallbackData { + envelope: Envelope; + rawProxyRequestBody: string; + rawSentryResponseBody: string; + sentryResponseStatusCode?: number; +} + +/** + * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` + * option to this server (like this `tunnel: http://localhost:${port option}/`). + */ +export async function startEventProxyServer(options: EventProxyServerOptions): Promise { + const eventCallbackListeners: Set<(data: string) => void> = new Set(); + + const proxyServer = http.createServer((proxyRequest, proxyResponse) => { + const proxyRequestChunks: Uint8Array[] = []; + + proxyRequest.addListener('data', (chunk: Buffer) => { + proxyRequestChunks.push(chunk); + }); + + proxyRequest.addListener('error', err => { + throw err; + }); + + proxyRequest.addListener('end', () => { + const proxyRequestBody = + proxyRequest.headers['content-encoding'] === 'gzip' + ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() + : Buffer.concat(proxyRequestChunks).toString(); + + let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); + + if (!envelopeHeader.dsn) { + throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); + } + + const { origin, pathname, host } = new URL(envelopeHeader.dsn); + + const projectId = pathname.substring(1); + const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; + + proxyRequest.headers.host = host; + + const sentryResponseChunks: Uint8Array[] = []; + + const sentryRequest = https.request( + sentryIngestUrl, + { headers: proxyRequest.headers, method: proxyRequest.method }, + sentryResponse => { + sentryResponse.addListener('data', (chunk: Buffer) => { + proxyResponse.write(chunk, 'binary'); + sentryResponseChunks.push(chunk); + }); + + sentryResponse.addListener('end', () => { + eventCallbackListeners.forEach(listener => { + const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); + + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody, + sentryResponseStatusCode: sentryResponse.statusCode, + }; + + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + proxyResponse.end(); + }); + + sentryResponse.addListener('error', err => { + throw err; + }); + + proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); + }, + ); + + sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); + sentryRequest.end(); + }); + }); + + const proxyServerStartupPromise = new Promise(resolve => { + proxyServer.listen(options.port, () => { + resolve(); + }); + }); + + const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { + eventCallbackResponse.statusCode = 200; + eventCallbackResponse.setHeader('connection', 'keep-alive'); + + const callbackListener = (data: string): void => { + eventCallbackResponse.write(data.concat('\n'), 'utf8'); + }; + + eventCallbackListeners.add(callbackListener); + + eventCallbackRequest.on('close', () => { + eventCallbackListeners.delete(callbackListener); + }); + + eventCallbackRequest.on('error', () => { + eventCallbackListeners.delete(callbackListener); + }); + }); + + const eventCallbackServerStartupPromise = new Promise(resolve => { + eventCallbackServer.listen(0, () => { + const port = String((eventCallbackServer.address() as AddressInfo).port); + void registerCallbackServerPort(options.proxyServerName, port).then(resolve); + }); + }); + + await eventCallbackServerStartupPromise; + await proxyServerStartupPromise; + return; +} + +export async function waitForRequest( + proxyServerName: string, + callback: (eventData: SentryRequestCallbackData) => Promise | boolean, +): Promise { + const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); + + return new Promise((resolve, reject) => { + const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { + let eventContents = ''; + + response.on('error', err => { + reject(err); + }); + + response.on('data', (chunk: Buffer) => { + const chunkString = chunk.toString('utf8'); + chunkString.split('').forEach(char => { + if (char === '\n') { + const eventCallbackData: SentryRequestCallbackData = JSON.parse( + Buffer.from(eventContents, 'base64').toString('utf8'), + ); + const callbackResult = callback(eventCallbackData); + if (typeof callbackResult !== 'boolean') { + callbackResult.then( + match => { + if (match) { + response.destroy(); + resolve(eventCallbackData); + } + }, + err => { + throw err; + }, + ); + } else if (callbackResult) { + response.destroy(); + resolve(eventCallbackData); + } + eventContents = ''; + } else { + eventContents = eventContents.concat(char); + } + }); + }); + }); + + request.end(); + }); +} + +export function waitForEnvelopeItem( + proxyServerName: string, + callback: (envelopeItem: EnvelopeItem) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForRequest(proxyServerName, async eventData => { + const envelopeItems = eventData.envelope[1]; + for (const envelopeItem of envelopeItems) { + if (await callback(envelopeItem)) { + resolve(envelopeItem); + return true; + } + } + return false; + }).catch(reject); + }); +} + +export function waitForError( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +const TEMP_FILE_PREFIX = 'event-proxy-server-'; + +async function registerCallbackServerPort(serverName: string, port: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + await writeFile(tmpFilePath, port, { encoding: 'utf8' }); +} + +function retrieveCallbackServerPort(serverName: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + return readFile(tmpFilePath, 'utf8'); +} diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json similarity index 90% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json rename to dev-packages/e2e-tests/test-applications/node-fastify-app/package.json index cfa2c8be0f61..c7ea9cac71ad 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json @@ -1,5 +1,5 @@ { - "name": "node-experimental-fastify-app", + "name": "node-fastify-app", "version": "1.0.0", "private": true, "scripts": { @@ -17,7 +17,6 @@ "@sentry/opentelemetry": "latest || *", "@types/node": "18.15.1", "fastify": "4.23.2", - "fastify-plugin": "4.5.1", "typescript": "4.9.5", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/playwright.config.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/playwright.config.ts diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js b/dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js similarity index 84% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js rename to dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js index 22f17ca4695c..7135fb33d91a 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js @@ -2,21 +2,12 @@ require('./tracing'); const Sentry = require('@sentry/node'); const { fastify } = require('fastify'); -const fastifyPlugin = require('fastify-plugin'); const http = require('http'); -const FastifySentry = fastifyPlugin(async (fastify, options) => { - fastify.decorateRequest('_sentryContext', null); - - fastify.addHook('onError', async (_request, _reply, error) => { - Sentry.captureException(error); - }); -}); - const app = fastify(); const port = 3030; -app.register(FastifySentry); +Sentry.setupFastifyErrorHandler(app); app.get('/test-success', function (req, res) { res.send({ version: 'v1' }); @@ -61,6 +52,10 @@ app.get('/test-error', async function (req, res) { res.send({ exceptionId }); }); +app.get('/test-exception', async function (req, res) { + throw new Error('This is an exception'); +}); + app.listen({ port: port }); function makeHttpRequest(url) { diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/tracing.js b/dev-packages/e2e-tests/test-applications/node-fastify-app/src/tracing.js similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/tracing.js rename to dev-packages/e2e-tests/test-applications/node-fastify-app/src/tracing.js diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts similarity index 66% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/start-event-proxy.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts index 7ae352993f3c..2ab9be450dcd 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts @@ -2,5 +2,5 @@ import { startEventProxyServer } from './event-proxy-server'; startEventProxyServer({ port: 3031, - proxyServerName: 'node-experimental-fastify-app', + proxyServerName: 'node-fastify-app', }); diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts similarity index 54% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts index 60bef3fb0607..b2ef0649472f 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; import axios, { AxiosError } from 'axios'; +import { waitForError } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; @@ -37,3 +38,35 @@ test('Sends exception to Sentry', async ({ baseURL }) => { ) .toBe(200); }); + +test('Sends correct error event', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-fastify-app', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception'; + }); + + try { + await axios.get(`${baseURL}/test-exception`); + } catch { + // this results in an error, but we don't care - we want to check the error event + } + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.any(String), + span_id: expect.any(String), + parent_span_id: expect.any(String), + }); +}); 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-fastify-app/tests/propagation.test.ts similarity index 94% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts index 0afc4ff09b06..89a6320f725a 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts @@ -4,14 +4,14 @@ import axios from 'axios'; import { waitForTransaction } from '../event-proxy-server'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-inbound-headers' ); }); - const outboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-outgoing-http' @@ -118,14 +118,14 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { }); test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-inbound-headers' ); }); - const outboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-outgoing-fetch' 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-fastify-app/tests/transactions.test.ts similarity index 95% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts index 4ff9b5df632c..54f6916de36c 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts @@ -8,7 +8,7 @@ const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; const EVENT_POLLING_TIMEOUT = 90_000; test('Sends an API route transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-transaction' @@ -58,13 +58,13 @@ test('Sends an API route transaction', async ({ baseURL }) => { spans: [ { data: { - 'plugin.name': 'fastify -> app-auto-0', + 'plugin.name': 'fastify -> sentry-fastify-error-handler', 'fastify.type': 'request_handler', 'http.route': '/test-transaction', 'otel.kind': 'INTERNAL', 'sentry.origin': 'auto.http.otel.fastify', }, - description: 'request handler - fastify -> app-auto-0', + description: 'request handler - fastify -> sentry-fastify-error-handler', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-fastify-app/tsconfig.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tsconfig.json rename to dev-packages/e2e-tests/test-applications/node-fastify-app/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/index.js b/dev-packages/e2e-tests/test-applications/node-profiling/index.js index be569e12f921..7fbe23ac7652 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling/index.js +++ b/dev-packages/e2e-tests/test-applications/node-profiling/index.js @@ -1,11 +1,11 @@ const Sentry = require('@sentry/node'); -const Profiling = require('@sentry/profiling-node'); +const { nodeProfilingIntegration } = require('@sentry/profiling-node'); const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); Sentry.init({ dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - integrations: [new Profiling.ProfilingIntegration()], + integrations: [nodeProfilingIntegration()], tracesSampleRate: 1.0, profilesSampleRate: 1.0, }); diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx index 73c5e024539f..579b6f8e1cfd 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx @@ -12,7 +12,7 @@ import { import Index from './pages/Index'; import User from './pages/User'; -const replay = new Sentry.Replay(); +const replay = Sentry.replayIntegration(); Sentry.init({ // environment: 'qa', // dynamic sampling bias to keep transactions diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tsconfig.json index c8df41dcf4b5..75ae036f46b0 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx index b8a036fc5340..bf68d694d0f7 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx @@ -12,7 +12,7 @@ import { import Index from './pages/Index'; import User from './pages/User'; -const replay = new Sentry.Replay(); +const replay = Sentry.replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tsconfig.json index c8df41dcf4b5..75ae036f46b0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/.gitignore b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/.gitignore deleted file mode 100644 index 84634c973eeb..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -/test-results/ -/playwright-report/ -/playwright/.cache/ - -!*.d.ts diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/.npmrc b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/.npmrc deleted file mode 100644 index 070f80f05092..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -@sentry:registry=http://127.0.0.1:4873 -@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json deleted file mode 100644 index cde5ad8225ee..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "standard-frontend-react-tracing-import-test", - "version": "0.1.0", - "private": true, - "dependencies": { - "@sentry/react": "latest || *", - "@testing-library/jest-dom": "5.14.1", - "@testing-library/react": "13.0.0", - "@testing-library/user-event": "13.2.1", - "@types/jest": "27.0.1", - "@types/node": "16.7.13", - "@types/react": "18.0.0", - "@types/react-dom": "18.0.0", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-router-dom": "^6.4.1", - "react-scripts": "5.0.1", - "typescript": "4.9.5", - "web-vitals": "2.1.0" - }, - "scripts": { - "build": "react-scripts build", - "start": "serve -s build", - "test": "playwright test", - "clean": "npx rimraf node_modules,pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:assert": "pnpm test" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "devDependencies": { - "@playwright/test": "1.26.1", - "axios": "1.6.0", - "serve": "14.0.1" - }, - "volta": { - "extends": "../../package.json" - } -} diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts deleted file mode 100644 index 5f93f826ebf0..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { PlaywrightTestConfig } from '@playwright/test'; -import { devices } from '@playwright/test'; - -/** - * See https://playwright.dev/docs/test-configuration. - */ -const config: PlaywrightTestConfig = { - testDir: './tests', - /* Maximum time one test can run for. */ - timeout: 150_000, - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 5000, - }, - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: 0, - /* Opt out of parallel tests on CI. */ - workers: 1, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'list', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 0, - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - }, - }, - // For now we only test Chrome! - // { - // name: 'firefox', - // use: { - // ...devices['Desktop Firefox'], - // }, - // }, - // { - // name: 'webkit', - // use: { - // ...devices['Desktop Safari'], - // }, - // }, - ], - - /* Run your local dev server before starting the tests */ - webServer: { - command: 'pnpm start', - port: 3030, - env: { - PORT: '3030', - }, - }, -}; - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/public/index.html b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/public/index.html deleted file mode 100644 index 39da76522bea..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/public/index.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - React App - - - -
- - - diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/globals.d.ts deleted file mode 100644 index 109dbcd55648..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/globals.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -interface Window { - recordedTransactions?: string[]; - capturedExceptionId?: string; -} diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/index.tsx b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/index.tsx deleted file mode 100644 index 319290d010ce..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as Sentry from '@sentry/react'; -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { - BrowserRouter, - Route, - Routes, - createRoutesFromChildren, - matchRoutes, - useLocation, - useNavigationType, -} from 'react-router-dom'; -import Index from './pages/Index'; -import User from './pages/User'; - -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.REACT_APP_E2E_TEST_DSN, - integrations: [ - Sentry.reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ], - // We recommend adjusting this value in production, or using tracesSampler - // for finer control - tracesSampleRate: 1.0, - release: 'e2e-test', -}); - -Sentry.addEventProcessor(event => { - if ( - event.type === 'transaction' && - (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') - ) { - const eventId = event.event_id; - if (eventId) { - window.recordedTransactions = window.recordedTransactions || []; - window.recordedTransactions.push(eventId); - } - } - - return event; -}); - -const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); - -const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); -root.render( - - - } /> - } /> - - , -); diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/Index.tsx deleted file mode 100644 index 7789a2773224..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/Index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as Sentry from '@sentry/react'; -// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX -import * as React from 'react'; -import { Link } from 'react-router-dom'; - -const Index = () => { - return ( - <> - { - const eventId = Sentry.captureException(new Error('I am an error!')); - window.capturedExceptionId = eventId; - }} - /> - - navigate - - - ); -}; - -export default Index; diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/User.tsx b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/User.tsx deleted file mode 100644 index 62f0c2d17533..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/User.tsx +++ /dev/null @@ -1,8 +0,0 @@ -// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX -import * as React from 'react'; - -const User = () => { - return

I am a blank page :)

; -}; - -export default User; diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/react-app-env.d.ts deleted file mode 100644 index 6431bc5fc6b2..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/react-app-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/tests/behaviour-test.spec.ts b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/tests/behaviour-test.spec.ts deleted file mode 100644 index 2e68363ab61a..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/tests/behaviour-test.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { expect, test } from '@playwright/test'; -import axios, { AxiosError } from 'axios'; - -const EVENT_POLLING_TIMEOUT = 90_000; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; - -test('Sends a pageload transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 1) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageLoadTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - try { - const response = await axios.get( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.data.contexts.trace.op === 'pageload') { - hadPageLoadTransaction = true; - } - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageLoadTransaction).toBe(true); -}); - -test('Sends a navigation transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - // Give pageload transaction time to finish - page.waitForTimeout(4000); - - const linkElement = page.locator('id=navigation'); - await linkElement.click(); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 2) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageNavigationTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - try { - const response = await axios.get( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.data.contexts.trace.op === 'navigation') { - hadPageNavigationTransaction = true; - } - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageNavigationTransaction).toBe(true); -}); diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/tsconfig.json deleted file mode 100644 index c8df41dcf4b5..000000000000 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react" - }, - "include": ["src", "tests"] -} diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx b/dev-packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx index 8cf0e8462e16..3a87a53ffdfa 100644 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx @@ -13,7 +13,7 @@ import { import Index from './pages/Index'; import User from './pages/User'; -const replay = new Sentry.Replay(); +const replay = Sentry.replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react/tsconfig.json b/dev-packages/e2e-tests/test-applications/standard-frontend-react/tsconfig.json index c8df41dcf4b5..75ae036f46b0 100644 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/standard-frontend-react/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts index 972ee59955dd..5240489b0934 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts @@ -60,6 +60,7 @@ test.describe('server-side errors', () => { }), ); - expect(errorEvent.transaction).toEqual('GET /server-route-error'); + // TODO: Uncomment once we update the scope transaction name on the server side + // expect(errorEvent.transaction).toEqual('GET /server-route-error'); }); }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts index f2b7d2d21531..c36d44c80068 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts @@ -63,6 +63,7 @@ test.describe('server-side errors', () => { }), ); - expect(errorEvent.transaction).toEqual('GET /server-route-error'); + // TODO: Uncomment once we update the scope transaction name on the server side + // expect(errorEvent.transaction).toEqual('GET /server-route-error'); }); }); diff --git a/dev-packages/e2e-tests/tracing-shim-esm/index.cjs b/dev-packages/e2e-tests/tracing-shim-esm/index.cjs deleted file mode 100644 index e245f86e5c40..000000000000 --- a/dev-packages/e2e-tests/tracing-shim-esm/index.cjs +++ /dev/null @@ -1,3 +0,0 @@ -// TODO(v8): Remove this file once we get rid of tracing dependency from sveltekit vite plugin -// This file is used as a shim for @sentry/tracing -module.exports = {}; diff --git a/dev-packages/e2e-tests/tracing-shim-esm/index.mjs b/dev-packages/e2e-tests/tracing-shim-esm/index.mjs deleted file mode 100644 index 5ba2afe713b3..000000000000 --- a/dev-packages/e2e-tests/tracing-shim-esm/index.mjs +++ /dev/null @@ -1,3 +0,0 @@ -// TODO(v8): Remove this file once we get rid of tracing dependency from sveltekit vite plugin -// This file is used as a shim for @sentry/tracing -export {}; diff --git a/dev-packages/e2e-tests/tracing-shim-esm/package.json b/dev-packages/e2e-tests/tracing-shim-esm/package.json deleted file mode 100644 index 577f7e2cef51..000000000000 --- a/dev-packages/e2e-tests/tracing-shim-esm/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "tracing-shim-esm", - "version": "0.0.1", - "private": true, - "main": "index.cjs", - "module": "index.mjs", - "scripts": {}, - "dependencies": {}, - "devDependencies": {} -} diff --git a/dev-packages/e2e-tests/tracing-shim/index.js b/dev-packages/e2e-tests/tracing-shim/index.js deleted file mode 100644 index e245f86e5c40..000000000000 --- a/dev-packages/e2e-tests/tracing-shim/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// TODO(v8): Remove this file once we get rid of tracing dependency from sveltekit vite plugin -// This file is used as a shim for @sentry/tracing -module.exports = {}; diff --git a/dev-packages/e2e-tests/tracing-shim/package.json b/dev-packages/e2e-tests/tracing-shim/package.json deleted file mode 100644 index 0ee353b22b5b..000000000000 --- a/dev-packages/e2e-tests/tracing-shim/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "tracing-shim", - "version": "0.0.1", - "private": true, - "main": "index.js", - "scripts": {}, - "dependencies": {}, - "devDependencies": {} -} diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index c99f9def69e4..851c99387b8c 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -134,7 +134,13 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! - '@sentry/serverless': + '@sentry/aws-serverless': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/google-cloud-serverless': access: $all publish: $all unpublish: $all diff --git a/dev-packages/node-integration-tests/README.md b/dev-packages/node-integration-tests/README.md index ec202b5a8252..35b3c10883b7 100644 --- a/dev-packages/node-integration-tests/README.md +++ b/dev-packages/node-integration-tests/README.md @@ -19,25 +19,37 @@ utils/ |---- server.ts [default Express server configuration] ``` -The tests are grouped by their scopes, such as `public-api` or `tracing`. In every group of tests, there are multiple folders containing test scenarios and assertions. +The tests are grouped by their scopes, such as `public-api` or `tracing`. In every group of tests, there are multiple +folders containing test scenarios and assertions. -Tests run on Express servers (a server instance per test). By default, a simple server template inside `utils/defaults/server.ts` is used. Every server instance runs on a different port. +Tests run on Express servers (a server instance per test). By default, a simple server template inside +`utils/defaults/server.ts` is used. Every server instance runs on a different port. -A custom server configuration can be used, supplying a script that exports a valid express server instance as default. `runServer` utility function accepts an optional `serverPath` argument for this purpose. +A custom server configuration can be used, supplying a script that exports a valid express server instance as default. +`runServer` utility function accepts an optional `serverPath` argument for this purpose. -`scenario.ts` contains the initialization logic and the test subject. By default, `{TEST_DIR}/scenario.ts` is used, but `runServer` also accepts an optional `scenarioPath` argument for non-standard usage. +`scenario.ts` contains the initialization logic and the test subject. By default, `{TEST_DIR}/scenario.ts` is used, but +`runServer` also accepts an optional `scenarioPath` argument for non-standard usage. -`test.ts` is required for each test case, and contains the server runner logic, request interceptors for Sentry requests, and assertions. Test server, interceptors and assertions are all run on the same Jest thread. +`test.ts` is required for each test case, and contains the server runner logic, request interceptors for Sentry +requests, and assertions. Test server, interceptors and assertions are all run on the same Jest thread. ### Utilities `utils/` contains helpers and Sentry-specific assertions that can be used in (`test.ts`). -`TestEnv` class contains methods to create and execute requests on a test server instance. `TestEnv.init()` which starts a test server and returns a `TestEnv` instance must be called by each test. The test server is automatically shut down after each test, if a data collection helper method such as `getEnvelopeRequest` and `getAPIResponse` is used. Tests that do not use those helper methods will need to end the server manually. +`TestEnv` class contains methods to create and execute requests on a test server instance. `TestEnv.init()` which starts +a test server and returns a `TestEnv` instance must be called by each test. The test server is automatically shut down +after each test, if a data collection helper method such as `getEnvelopeRequest` and `getAPIResponse` is used. Tests +that do not use those helper methods will need to end the server manually. -`TestEnv` instance has two public properties: `url` and `server`. The `url` property is the base URL for the server. The `http.Server` instance is used to finish the server eventually. +`TestEnv` instance has two public properties: `url` and `server`. The `url` property is the base URL for the server. The +`http.Server` instance is used to finish the server eventually. -Nock interceptors are internally used to capture envelope requests by `getEnvelopeRequest` and `getMultipleEnvelopeRequest` helpers. After capturing required requests, the interceptors are removed. Nock can manually be used inside the test cases to intercept requests but should be removed before the test ends, as not to cause flakiness. +Nock interceptors are internally used to capture envelope requests by `getEnvelopeRequest` and +`getMultipleEnvelopeRequest` helpers. After capturing required requests, the interceptors are removed. Nock can manually +be used inside the test cases to intercept requests but should be removed before the test ends, as not to cause +flakiness. ## Running Tests Locally diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index aaf7d46fba5f..1b16797f102d 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -3,7 +3,7 @@ "version": "8.0.0-alpha.2", "license": "MIT", "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "private": true, "main": "build/cjs/index.js", diff --git a/dev-packages/overhead-metrics/.eslintrc.cjs b/dev-packages/overhead-metrics/.eslintrc.cjs index a4f2d4fdd936..3eed32128e5c 100644 --- a/dev-packages/overhead-metrics/.eslintrc.cjs +++ b/dev-packages/overhead-metrics/.eslintrc.cjs @@ -9,7 +9,6 @@ module.exports = { '@typescript-eslint/no-non-null-assertion': 'off', '@sentry-internal/sdk/no-optional-chaining': 'off', '@sentry-internal/sdk/no-nullish-coalescing': 'off', - '@sentry-internal/sdk/no-unsupported-es6-methods': 'off', '@sentry-internal/sdk/no-class-field-initializers': 'off', 'jsdoc/require-jsdoc': 'off', }, diff --git a/dev-packages/overhead-metrics/README.md b/dev-packages/overhead-metrics/README.md index d7aa5fab0534..51e7d2587ac0 100644 --- a/dev-packages/overhead-metrics/README.md +++ b/dev-packages/overhead-metrics/README.md @@ -1,11 +1,13 @@ # Overhead performance metrics -Evaluates Sentry & Replay impact on website performance by running a web app in Chromium via Playwright and collecting various metrics. +Evaluates Sentry & Replay impact on website performance by running a web app in Chromium via Playwright and collecting +various metrics. -The general idea is to run a web app without Sentry, and then run the same app again with Sentry and another one with Sentry+Replay included. -For the three scenarios, we collect some metrics (CPU, memory, vitals) and later compare them and post as a comment in a PR. -Changes in the metrics, compared to previous runs from the main branch, should be evaluated on case-by-case basis when preparing and reviewing the PR. +The general idea is to run a web app without Sentry, and then run the same app again with Sentry and another one with +Sentry+Replay included. For the three scenarios, we collect some metrics (CPU, memory, vitals) and later compare them +and post as a comment in a PR. Changes in the metrics, compared to previous runs from the main branch, should be +evaluated on case-by-case basis when preparing and reviewing the PR. ## Resources -* https://github.com/addyosmani/puppeteer-webperf +- https://github.com/addyosmani/puppeteer-webperf diff --git a/dev-packages/overhead-metrics/configs/README.md b/dev-packages/overhead-metrics/configs/README.md index cb9724ba4619..ceb96835f975 100644 --- a/dev-packages/overhead-metrics/configs/README.md +++ b/dev-packages/overhead-metrics/configs/README.md @@ -1,4 +1,4 @@ # Replay metrics configuration & entrypoints (scripts) -* [dev](dev) contains scripts launched during local development -* [ci](ci) contains scripts launched in CI +- [dev](dev) contains scripts launched during local development +- [ci](ci) contains scripts launched in CI diff --git a/dev-packages/overhead-metrics/test-apps/booking-app/with-replay.html b/dev-packages/overhead-metrics/test-apps/booking-app/with-replay.html index 21f19f6260be..ae99a6171f0c 100644 --- a/dev-packages/overhead-metrics/test-apps/booking-app/with-replay.html +++ b/dev-packages/overhead-metrics/test-apps/booking-app/with-replay.html @@ -224,7 +224,7 @@

This is a test app.

enableTracing: true, integrations: [ Sentry.browserTracingIntegration(), - new Sentry.Integrations.Replay({ + Sentry.replayIntegration({ useCompression: true, flushMinDelay: 2000, flushMaxDelay: 2000, diff --git a/dev-packages/overhead-metrics/test-apps/jank/README.md b/dev-packages/overhead-metrics/test-apps/jank/README.md index 3e0f46b66a1e..beb81a4eadd2 100644 --- a/dev-packages/overhead-metrics/test-apps/jank/README.md +++ b/dev-packages/overhead-metrics/test-apps/jank/README.md @@ -1,4 +1,6 @@ # Chrome DevTools Jank article sample code -* Originally coming from [devtools-samples](https://github.com/GoogleChrome/devtools-samples/tree/4818abc9dbcdb954d0eb9b70879f4ea18756451f/jank), licensed under Apache 2.0. -* Linking article: +- Originally coming from + [devtools-samples](https://github.com/GoogleChrome/devtools-samples/tree/4818abc9dbcdb954d0eb9b70879f4ea18756451f/jank), + licensed under Apache 2.0. +- Linking article: diff --git a/dev-packages/overhead-metrics/test-apps/jank/with-replay.html b/dev-packages/overhead-metrics/test-apps/jank/with-replay.html index 16e1a6a6ea47..6c5f32cc7e8d 100644 --- a/dev-packages/overhead-metrics/test-apps/jank/with-replay.html +++ b/dev-packages/overhead-metrics/test-apps/jank/with-replay.html @@ -31,7 +31,7 @@ dsn: 'https://d16ae2d36f9249849c7964e9a3a8a608@o447951.ingest.sentry.io/5429213', replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 1.0, - integrations: [new Sentry.Integrations.Replay({})], + integrations: [Sentry.replayIntegration({})], }); diff --git a/dev-packages/rollup-utils/README.md b/dev-packages/rollup-utils/README.md index 2d79f3eabeec..ff38b562ced2 100644 --- a/dev-packages/rollup-utils/README.md +++ b/dev-packages/rollup-utils/README.md @@ -1,5 +1,7 @@ # The `rollup-utils` Package -This is a small utility packages for all the Rollup configurations we have in this project. It contains helpers to create standardized configs, custom rollup plugins, and other things that might have to do with the build process like polyfill snippets. +This is a small utility packages for all the Rollup configurations we have in this project. It contains helpers to +create standardized configs, custom rollup plugins, and other things that might have to do with the build process like +polyfill snippets. This package will not be published and is only intended to be used inside this repository. diff --git a/dev-packages/rollup-utils/bundleHelpers.mjs b/dev-packages/rollup-utils/bundleHelpers.mjs index 6043207ee97c..ae6574c0b0bc 100644 --- a/dev-packages/rollup-utils/bundleHelpers.mjs +++ b/dev-packages/rollup-utils/bundleHelpers.mjs @@ -7,17 +7,16 @@ import { builtinModules } from 'module'; import deepMerge from 'deepmerge'; import { - getEs5Polyfills, makeBrowserBuildPlugin, makeCleanupPlugin, makeCommonJSPlugin, makeIsDebugBuildPlugin, + makeJsonPlugin, makeLicensePlugin, makeNodeResolvePlugin, makeRrwebBuildPlugin, makeSetSDKSourcePlugin, makeSucrasePlugin, - makeTSPlugin, makeTerserPlugin, } from './plugins/index.mjs'; import { mergePlugins } from './utils.mjs'; @@ -25,16 +24,13 @@ import { mergePlugins } from './utils.mjs'; const BUNDLE_VARIANTS = ['.js', '.min.js', '.debug.min.js']; export function makeBaseBundleConfig(options) { - const { bundleType, entrypoints, jsVersion, licenseTitle, outputFileBase, packageSpecificConfig } = options; - - const isEs5 = jsVersion.toLowerCase() === 'es5'; + const { bundleType, entrypoints, licenseTitle, outputFileBase, packageSpecificConfig, sucrase } = options; const nodeResolvePlugin = makeNodeResolvePlugin(); - const sucrasePlugin = makeSucrasePlugin(); + const sucrasePlugin = makeSucrasePlugin(sucrase); const cleanupPlugin = makeCleanupPlugin(); const markAsBrowserBuildPlugin = makeBrowserBuildPlugin(true); const licensePlugin = makeLicensePlugin(licenseTitle); - const tsPlugin = makeTSPlugin('es5'); const rrwebBuildPlugin = makeRrwebBuildPlugin({ excludeIframe: false, excludeShadowDom: false, @@ -45,15 +41,13 @@ export function makeBaseBundleConfig(options) { // at all, and without `transformMixedEsModules`, they're only included if they're imported, not if they're required.) const commonJSPlugin = makeCommonJSPlugin({ transformMixedEsModules: true }); + const jsonPlugin = makeJsonPlugin(); + // used by `@sentry/browser` const standAloneBundleConfig = { output: { format: 'iife', name: 'Sentry', - outro: () => { - // Add polyfills for ES6 array/string methods at the end of the bundle - return isEs5 ? getEs5Polyfills() : ''; - }, }, context: 'window', plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin], @@ -93,12 +87,12 @@ export function makeBaseBundleConfig(options) { plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin], }; - // used by `@sentry/serverless`, when creating the lambda layer + // used by `@sentry/aws-serverless`, when creating the lambda layer const nodeBundleConfig = { output: { format: 'cjs', }, - plugins: [commonJSPlugin], + plugins: [jsonPlugin, commonJSPlugin], // Don't bundle any of Node's core modules external: builtinModules, }; @@ -123,9 +117,7 @@ export function makeBaseBundleConfig(options) { strict: false, esModule: false, }, - plugins: isEs5 - ? [tsPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin] - : [sucrasePlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin], + plugins: [sucrasePlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin], treeshake: 'smallest', }; diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 6085a502200f..58bb4d75edf7 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -17,6 +17,7 @@ import { makeSetSDKSourcePlugin, makeSucrasePlugin, } from './plugins/index.mjs'; +import { makePackageNodeEsm } from './plugins/make-esm-plugin.mjs'; import { mergePlugins } from './utils.mjs'; const packageDotJSON = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), './package.json'), { encoding: 'utf8' })); @@ -28,10 +29,11 @@ export function makeBaseNPMConfig(options = {}) { hasBundles = false, packageSpecificConfig = {}, addPolyfills = true, + sucrase = {}, } = options; const nodeResolvePlugin = makeNodeResolvePlugin(); - const sucrasePlugin = makeSucrasePlugin({ disableESTransforms: !addPolyfills }); + const sucrasePlugin = makeSucrasePlugin({ disableESTransforms: !addPolyfills, ...sucrase }); const debugBuildStatementReplacePlugin = makeDebugBuildStatementReplacePlugin(); const cleanupPlugin = makeCleanupPlugin(); const extractPolyfillsPlugin = makeExtractPolyfillsPlugin(); @@ -50,6 +52,10 @@ export function makeBaseNPMConfig(options = {}) { sourcemap: true, + // Include __esModule property when generating exports + // Before the upgrade to Rollup 4 this was included by default and when it was gone it broke tests + esModule: true, + // output individual files rather than one big bundle preserveModules: true, @@ -104,6 +110,7 @@ export function makeBaseNPMConfig(options = {}) { ...builtinModules, ...Object.keys(packageDotJSON.dependencies || {}), ...Object.keys(packageDotJSON.peerDependencies || {}), + ...Object.keys(packageDotJSON.optionalDependencies || {}), ], }; @@ -117,11 +124,16 @@ export function makeBaseNPMConfig(options = {}) { }); } -export function makeNPMConfigVariants(baseConfig) { - const variantSpecificConfigs = [ - { output: { format: 'cjs', dir: path.join(baseConfig.output.dir, 'cjs') } }, - { output: { format: 'esm', dir: path.join(baseConfig.output.dir, 'esm') } }, - ]; +export function makeNPMConfigVariants(baseConfig, options = {}) { + const { emitEsm = true } = options; + + const variantSpecificConfigs = [{ output: { format: 'cjs', dir: path.join(baseConfig.output.dir, 'cjs') } }]; + + if (emitEsm) { + variantSpecificConfigs.push({ + output: { format: 'esm', dir: path.join(baseConfig.output.dir, 'esm'), plugins: [makePackageNodeEsm()] }, + }); + } return variantSpecificConfigs.map(variant => deepMerge(baseConfig, variant)); } diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index 1df1e2ac917d..80dbc4422ea4 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -9,17 +9,12 @@ */ import * as childProcess from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import { fileURLToPath } from 'url'; import commonjs from '@rollup/plugin-commonjs'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; -import typescript from '@rollup/plugin-typescript'; -import deepMerge from 'deepmerge'; +import terser from '@rollup/plugin-terser'; import license from 'rollup-plugin-license'; -import { terser } from 'rollup-plugin-terser'; /** * Create a plugin to add an identification banner to the top of stand-alone bundles. @@ -43,10 +38,6 @@ export function makeLicensePlugin(title) { return plugin; } -export function getEs5Polyfills() { - return fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '../polyfills/es5.js'), 'utf-8'); -} - /** * Create a plugin to set the value of the `__SENTRY_DEBUG__` magic string. * @@ -133,6 +124,9 @@ export function makeTerserPlugin() { // For v7 backwards-compatibility we need to access txn._frozenDynamicSamplingContext // TODO (v8): Remove this reserved word '_frozenDynamicSamplingContext', + // These are used to keep span relationships + '_sentryRootSpan', + '_sentryChildSpans', ], }, }, @@ -142,45 +136,6 @@ export function makeTerserPlugin() { }); } -/** - * Create a TypeScript plugin, which will down-compile if necessary, based on the given JS version. - * - * @param jsVersion Either `es5` or `es6` - * @returns An instance of the `typescript` plugin - */ -export function makeTSPlugin(jsVersion) { - const baseTSPluginOptions = { - tsconfig: 'tsconfig.json', - compilerOptions: { - declaration: false, - declarationMap: false, - paths: { - '@sentry/browser': ['../browser/src'], - '@sentry/core': ['../core/src'], - '@sentry/types': ['../types/src'], - '@sentry/utils': ['../utils/src'], - '@sentry-internal/integration-shims': ['../integration-shims/src'], - '@sentry-internal/tracing': ['../tracing-internal/src'], - }, - baseUrl: '.', - }, - include: ['*.ts+(|x)', '**/*.ts+(|x)', '../**/*.ts+(|x)'], - }; - - const plugin = typescript( - deepMerge(baseTSPluginOptions, { - compilerOptions: { - target: jsVersion, - }, - }), - ); - - // give it a nicer name for later, when we'll need to sort the plugins - plugin.name = 'typescript'; - - return plugin; -} - // We don't pass these plugins any options which need to be calculated or changed by us, so no need to wrap them in // another factory function, as they are themselves already factory functions. diff --git a/dev-packages/rollup-utils/plugins/make-esm-plugin.mjs b/dev-packages/rollup-utils/plugins/make-esm-plugin.mjs new file mode 100644 index 000000000000..aa3f272ba2e0 --- /dev/null +++ b/dev-packages/rollup-utils/plugins/make-esm-plugin.mjs @@ -0,0 +1,16 @@ +/** + * Outputs a package.json file with {type: module} in the root of the output directory so that Node + * treats .js files as ESM. + */ +export function makePackageNodeEsm() { + return { + name: 'make-package-node-esm', + generateBundle() { + this.emitFile({ + type: 'asset', + fileName: 'package.json', + source: '{ "type": "module" }', + }); + }, + }; +} diff --git a/dev-packages/rollup-utils/plugins/npmPlugins.mjs b/dev-packages/rollup-utils/plugins/npmPlugins.mjs index 507480c1dd43..7138964a919b 100644 --- a/dev-packages/rollup-utils/plugins/npmPlugins.mjs +++ b/dev-packages/rollup-utils/plugins/npmPlugins.mjs @@ -7,6 +7,7 @@ * Sucrase plugin docs: https://github.com/rollup/plugins/tree/master/packages/sucrase */ +import json from '@rollup/plugin-json'; import replace from '@rollup/plugin-replace'; import sucrase from '@rollup/plugin-sucrase'; import cleanup from 'rollup-plugin-cleanup'; @@ -18,11 +19,17 @@ import cleanup from 'rollup-plugin-cleanup'; */ export function makeSucrasePlugin(options = {}) { return sucrase({ + // Required for bundling OTEL code properly + exclude: ['**/*.json'], transforms: ['typescript', 'jsx'], ...options, }); } +export function makeJsonPlugin() { + return json(); +} + /** * Create a plugin which can be used to pause the build process at the given hook. * diff --git a/dev-packages/rollup-utils/polyfills/es5.js b/dev-packages/rollup-utils/polyfills/es5.js deleted file mode 100644 index 54bd46e62cff..000000000000 --- a/dev-packages/rollup-utils/polyfills/es5.js +++ /dev/null @@ -1,41 +0,0 @@ -// Sentry ES5 polyfills -if (!('includes' in Array.prototype)) { - Array.prototype.includes = function (searchElement) { - return this.indexOf(searchElement) > -1; - }; -} -if (!('find' in Array.prototype)) { - Array.prototype.find = function (callback) { - for (var i = 0; i < this.length; i++) { - if (callback(this[i])) { - return this[i]; - } - } - }; -} -if (!('findIndex' in Array.prototype)) { - Array.prototype.findIndex = function (callback) { - for (var i = 0; i < this.length; i++) { - if (callback(this[i])) { - return i; - } - } - return -1; - }; -} -if (!('includes' in String.prototype)) { - String.prototype.includes = function (searchElement) { - return this.indexOf(searchElement) > -1; - }; -} -if (!('startsWith' in String.prototype)) { - String.prototype.startsWith = function (searchElement) { - return this.indexOf(searchElement) === 0; - }; -} -if (!('endsWith' in String.prototype)) { - String.prototype.endsWith = function (searchElement) { - var i = this.indexOf(searchElement); - return i > -1 && i === this.length - searchElement.length; - }; -} diff --git a/docs/migration/v4-to-v5_v6.md b/docs/migration/v4-to-v5_v6.md index 6928022eef20..4fcf8c1410ea 100644 --- a/docs/migration/v4-to-v5_v6.md +++ b/docs/migration/v4-to-v5_v6.md @@ -1,6 +1,7 @@ # Upgrading from 4.x to 5.x/6.x -We recommend upgrading from `4.x` to `6.x` directly. Migrating from `5.x` to `6.x` has no breaking changes to the SDK's API. +We recommend upgrading from `4.x` to `6.x` directly. Migrating from `5.x` to `6.x` has no breaking changes to the SDK's +API. In this version upgrade, there are a few breaking changes. This guide should help you update your code accordingly. diff --git a/docs/migration/v6-to-v7.md b/docs/migration/v6-to-v7.md index ac6ce0519e8e..8bd0da2b7a8e 100644 --- a/docs/migration/v6-to-v7.md +++ b/docs/migration/v6-to-v7.md @@ -541,4 +541,3 @@ const levelA = Severity.error; const levelB: SeverityLevel = "error" ``` - diff --git a/docs/new-sdk-release-checklist.md b/docs/new-sdk-release-checklist.md index 99c952631f01..a6e0f60e8235 100644 --- a/docs/new-sdk-release-checklist.md +++ b/docs/new-sdk-release-checklist.md @@ -59,9 +59,6 @@ differ slightly for other SDKs depending on how they are structured and how they - **This is especially important, if you're adding new CDN bundles!** - Tarballs (\*.tgz archives) should work OOTB -- [ ] Make sure it is added to `bundlePlugins.ts:makeTSPlugin` as `paths`, otherwise it will not be ES5 transpiled - correctly for CDN builds. - - [ ] Make sure it is added to the [Verdaccio config](https://github.com/getsentry/sentry-javascript/blob/develop/dev-packages/e2e-tests/verdaccio-config/config.yaml) for the E2E tests diff --git a/docs/v8-new-performance-apis.md b/docs/v8-new-performance-apis.md index cdf7bc6f9d0c..713e99d5d1f1 100644 --- a/docs/v8-new-performance-apis.md +++ b/docs/v8-new-performance-apis.md @@ -55,12 +55,8 @@ interface SpanContext { name: string; attributes?: SpanAttributes; op?: string; - // TODO: Not yet implemented, but you should be able to pass a scope to base this off scope?: Scope; - // TODO: The list below may change a bit... - origin?: SpanOrigin; - source?: SpanSource; - metadata?: Partial; + forceTransaction?: boolean; } ``` diff --git a/docs/v8-node.md b/docs/v8-node.md new file mode 100644 index 000000000000..7b5e7ce48b95 --- /dev/null +++ b/docs/v8-node.md @@ -0,0 +1,197 @@ +# Using `@sentry/node` in v8 + +With v8, `@sentry/node` has been completely overhauled. It is now powered by [OpenTelemetry](https://opentelemetry.io/) +under the hood. + +## What is OpenTelemetry + +You do not need to know or understand what OpenTelemetry is in order to use Sentry. We set up OpenTelemetry under the +hood, no knowledge of it is required in order to get started. + +If you want, you can use OpenTelemetry-native APIs to start spans, and Sentry will pick up everything automatically. + +## Supported Frameworks & Libraries + +We support the following Node Frameworks out of the box: + +- [Express](#express) +- [Fastify](#fastify) +- Koa +- Nest.js +- Hapi + +We also support auto instrumentation for the following libraries: + +- mysql +- mysql2 +- pg +- GraphQL (including Apollo Server) +- mongo +- mongoose +- Prisma + +## General Changes to v7 + +There are some general changes that have been made that apply to any usage of `@sentry/node`. + +### `Sentry.init()` has to be called before any other require/import + +Due to the way that OpenTelemetry auto instrumentation works, it is required that you initialize Sentry _before_ you +require or import any other package. Any package that is required/imported before Sentry is initialized may not be +correctly auto-instrumented. + +```js +// In v7, this was fine: +const Sentry = require('@sentry/node'); +const express = require('express'); + +Sentry.init({ + // ... +}); + +const app = express(); +``` + +```js +// In v8, in order to ensure express is instrumented, +// you have to initialize before you import: +const Sentry = require('@sentry/node'); +Sentry.init({ + // ... +}); + +const express = require('express'); +const app = express(); +``` + +### Performance Instrumentation is enabled by default + +All performance auto-instrumentation will be automatically enabled if the package is found. You do not need to add any +integration yourself, and `autoDiscoverNodePerformanceMonitoringIntegrations()` has also been removed. + +### Old Performance APIs are removed + +See [New Performance APIs](./v8-new-performance-apis.md) for details. + +### ESM Support + +For now, ESM support is only experimental. For the time being we only fully support CJS-based Node application - we are +working on this during the v8 alpha/beta cycle. + +### Using Custom OpenTelemetry Instrumentation + +While we include some vetted OpenTelemetry instrumentation out of the box, you can also add your own instrumentation on +top of that. You can do that by installing an instrumentation package (as well as `@opentelemetry/instrumentation`) and +setting it up like this: + +```js +const Sentry = require('@sentry/node'); +const { GenericPoolInstrumentation } = require('@opentelemetry/instrumentation-generic-pool'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +Sentry.init({ + dsn: '__DSN__', +}); + +// Afterwards, you can add additional instrumentation: +registerInsturmentations({ + instrumentations: [new GenericPoolInstrumentation()], +}); +``` + +### Using a Custom OpenTelemetry Setup + +If you already have OpenTelemetry set up yourself, you can also use your existing setup. + +In this case, you need to set `skipOpenTelemetrySetup: true` in your `init({})` config, and ensure you setup all the +components that Sentry needs yourself. In this case, you need to install `@sentry/opentelemetry`, and add the following: + +```js +const Sentry = require('@sentry/node'); +const { SentrySpanProcessor, SentryPropagator, SentryContextManager, SentrySampler } = require('@sentry/opentelemetry'); + +// We need a custom span processor +provider.addSpanProcessor(new SentrySpanProcessor()); +// We need a custom propagator and context manager +provier.register({ + propagator: new SentryPropagator(), + contextManager: new SentryContextManager(), +}); + +// And optionally, if you want to use the `tracesSamplingRate` or related options from Sentry, +// you also need to use a custom sampler when you set up your provider +const provider = new BasicTracerProvider({ + sampler: new SentrySampler(Sentry.getClient()), +}); +``` + +## Plain Node / Unsupported Frameworks + +When using `@sentry/node` in an app without any supported framework, you will still get some auto instrumentation out of +the box! + +Any framework that works on top of `http`, which means any framework that handles incoming HTTP requests, will +automatically be instrumented - so you'll get request isolation & basic transactions without any further action. + +For any non-HTTP scenarios (e.g. websockets or a scheduled job), you'll have to manually ensure request isolation by +wrapping the function with `Sentry.withIsolationScope()`: + +```js +const Sentry = require('@sentry/node'); + +function myScheduledJob() { + return Sentry.withIsolationScope(async () => { + await doSomething(); + await doSomethingElse(); + return { status: 'DONE' }; + }); +} +``` + +This way, anything happening inside of this function will be isolated, even if they run concurrently. + +## Express + +The following shows how you can setup Express instrumentation in v8. This will capture performance data & errors for +your Express app. + +```js +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: '__DSN__', + tracesSampleRate: 1, +}); + +const express = require('express'); +const app = express(); + +// add routes etc. here + +Sentry.setupExpressErrorHandler(app); +// add other error middleware below this, if needed + +app.listen(3000); +``` + +## Fastify + +The following shows how you can setup Fastify instrumentation in v8. This will capture performance data & errors for +your Fastify app. + +```js +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: '__DSN__', + tracesSampleRate: 1, +}); + +const { fastify } = require('fastify'); +const app = fastify(); +Sentry.setupFastifyErrorHandler(app); + +// add routes etc. here + +app.listen(); +``` diff --git a/jest/jest.config.js b/jest/jest.config.js index 37a2ee48658b..6bb8d30df35e 100644 --- a/jest/jest.config.js +++ b/jest/jest.config.js @@ -19,4 +19,11 @@ module.exports = { __DEBUG_BUILD__: true, }, testPathIgnorePatterns: ['/build/', '/node_modules/'], + + // On CI, we do not need the pretty CLI output, as it makes logs harder to parse + ...(process.env.CI + ? { + coverageReporters: ['json', 'lcov', 'clover'], + } + : {}), }; diff --git a/package.json b/package.json index 0c39a0033abb..28d2fb4e74a3 100644 --- a/package.json +++ b/package.json @@ -22,19 +22,18 @@ "fix": "run-s fix:biome fix:prettier fix:lerna", "fix:lerna": "lerna run fix", "fix:biome": "biome check --apply .", - "fix:prettier": "prettier **/*.md *.md **/*.css --write", + "fix:prettier": "prettier \"**/*.md\" \"**/*.css\" --write", "changelog": "ts-node ./scripts/get-commit-list.ts", "link:yarn": "lerna exec yarn link", "lint": "run-s lint:lerna lint:biome lint:prettier", "lint:clang": "lerna run lint:clang", "lint:lerna": "lerna run lint", "lint:biome": "biome check .", - "lint:prettier": "prettier **/*.md *.md **/*.css --check", - "validate:es5": "lerna run validate:es5", + "lint:prettier": "prettier \"**/*.md\" \"**/*.css\" --check", "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", "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test:unit", - "test-ci-browser": "lerna run test --ignore \"@sentry/{bun,deno,node,node-experimental,profiling-node,serverless,nextjs,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", + "test-ci-browser": "lerna run test --ignore \"@sentry/{bun,deno,node,node-experimental,profiling-node,serverless,google-cloud,nextjs,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", "test-ci-node": "ts-node ./scripts/node-unit-tests.ts", "test-ci-bun": "lerna run test --scope @sentry/bun", "test:update-snapshots": "lerna run test:update-snapshots", @@ -48,6 +47,7 @@ "packages/angular", "packages/angular-ivy", "packages/astro", + "packages/aws-serverless", "packages/browser", "packages/bun", "packages/core", @@ -57,6 +57,7 @@ "packages/eslint-plugin-sdk", "packages/feedback", "packages/gatsby", + "packages/google-cloud-serverless", "packages/integration-shims", "packages/nextjs", "packages/node", @@ -68,7 +69,6 @@ "packages/replay", "packages/replay-canvas", "packages/replay-worker", - "packages/serverless", "packages/svelte", "packages/sveltekit", "packages/tracing-internal", @@ -86,11 +86,13 @@ ], "devDependencies": { "@biomejs/biome": "^1.4.0", - "@rollup/plugin-commonjs": "^21.0.1", - "@rollup/plugin-node-resolve": "^13.1.3", - "@rollup/plugin-replace": "^3.0.1", - "@rollup/plugin-sucrase": "^4.0.3", - "@rollup/plugin-typescript": "^8.3.1", + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-sucrase": "^5.0.2", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^11.1.6", "@size-limit/file": "~11.0.1", "@size-limit/webpack": "~11.0.1", "@strictsoftware/typedoc-plugin-monorepo": "^0.3.1", @@ -119,10 +121,9 @@ "prettier": "^3.1.1", "replace-in-file": "^4.0.0", "rimraf": "^3.0.2", - "rollup": "^2.67.1", - "rollup-plugin-cleanup": "3.2.1", - "rollup-plugin-license": "^2.6.1", - "rollup-plugin-terser": "^7.0.2", + "rollup": "^4.13.0", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-license": "^3.3.1", "sinon": "^7.3.2", "size-limit": "~11.0.1", "ts-jest": "^27.1.4", @@ -133,7 +134,8 @@ "yalc": "^1.0.0-pre.53" }, "resolutions": { - "**/agent-base": "5", + "wrap-ansi": "7.0.0", + "string-width": "4.1.0", "**/terser/source-map": "0.7.4" }, "version": "0.0.0", diff --git a/packages/angular-ivy/README.md b/packages/angular-ivy/README.md index 438bb0fb5ff5..6252cec45454 100644 --- a/packages/angular-ivy/README.md +++ b/packages/angular-ivy/README.md @@ -18,10 +18,11 @@ This SDK officially supports Angular 12 to 17 with Angular's new rendering engine, Ivy. -If you're using Angular 10, 11 or a newer Angular version with View Engine instead of Ivy, please use [`@sentry/angular`](https://github.com/getsentry/sentry-javascript/blob/develop/packages/angular/README.md). +If you're using Angular 10, 11 or a newer Angular version with View Engine instead of Ivy, please use +[`@sentry/angular`](https://github.com/getsentry/sentry-javascript/blob/develop/packages/angular/README.md). -If you're using an older version of Angular and experience problems with the Angular SDK, we recommend downgrading the SDK to version 6.x. -Please note that we don't provide any support for Angular versions below 10. +If you're using an older version of Angular and experience problems with the Angular SDK, we recommend downgrading the +SDK to version 6.x. Please note that we don't provide any support for Angular versions below 10. ## General @@ -53,8 +54,8 @@ platformBrowserDynamic() ### ErrorHandler -`@sentry/angular-ivy` exports a function to instantiate an ErrorHandler provider that will automatically send Javascript errors -captured by the Angular's error handler. +`@sentry/angular-ivy` exports a function to instantiate an ErrorHandler provider that will automatically send Javascript +errors captured by the Angular's error handler. ```javascript import { NgModule, ErrorHandler } from '@angular/core'; @@ -89,17 +90,14 @@ initializations. Registering a Trace Service is a 3-step process. -1. Register and configure the `BrowserTracing` integration, including custom Angular routing - instrumentation: +1. Register and configure the `BrowserTracing` integration, including custom Angular routing instrumentation: ```javascript import { init, browserTracingIntegration } from '@sentry/angular-ivy'; init({ dsn: '__DSN__', - integrations: [ - browserTracingIntegration(), - ], + integrations: [browserTracingIntegration()], tracePropagationTargets: ['localhost', 'https://yourserver.io/api'], tracesSampleRate: 1, }); @@ -181,39 +179,40 @@ Then, inside your component's template (keep in mind that the directive's name a ``` -_TraceClassDecorator:_ used to track a duration between `OnInit` and `AfterViewInit` lifecycle hooks in components: +_TraceClass:_ used to track a duration between `OnInit` and `AfterViewInit` lifecycle hooks in components: ```javascript import { Component } from '@angular/core'; -import { TraceClassDecorator } from '@sentry/angular-ivy'; +import { TraceClass } from '@sentry/angular-ivy'; @Component({ selector: 'layout-header', templateUrl: './header.component.html', }) -@TraceClassDecorator() +@TraceClass() export class HeaderComponent { // ... } ``` -_TraceMethodDecorator:_ used to track a specific lifecycle hooks as point-in-time spans in components: +_TraceMethod:_ used to track a specific lifecycle hooks as point-in-time spans in components: ```javascript import { Component, OnInit } from '@angular/core'; -import { TraceMethodDecorator } from '@sentry/angular-ivy'; +import { TraceMethod } from '@sentry/angular-ivy'; @Component({ selector: 'app-footer', templateUrl: './footer.component.html', }) export class FooterComponent implements OnInit { - @TraceMethodDecorator() + @TraceMethod() ngOnInit() {} } ``` -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: +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'; @@ -223,12 +222,13 @@ import { init, startSpan } from '@sentry/angular'; import { AppModule } from './app/app.module'; // ... -startSpan({ - name: 'platform-browser-dynamic', - op: 'ui.angular.bootstrap' +startSpan( + { + name: 'platform-browser-dynamic', + op: 'ui.angular.bootstrap', }, async () => { await platformBrowserDynamic().bootstrapModule(AppModule); - } + }, ); ``` diff --git a/packages/angular-ivy/package.json b/packages/angular-ivy/package.json index 77377f9b26c4..5f2cf0c9813d 100644 --- a/packages/angular-ivy/package.json +++ b/packages/angular-ivy/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "main": "build/bundles/sentry-angular.umd.js", "module": "build/fesm2015/sentry-angular.js", diff --git a/packages/angular-ivy/tsconfig.ngc.json b/packages/angular-ivy/tsconfig.ngc.json index eb826fa42ff9..8a1d95d7a256 100644 --- a/packages/angular-ivy/tsconfig.ngc.json +++ b/packages/angular-ivy/tsconfig.ngc.json @@ -5,7 +5,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "target": "es2015", + "target": "es2017", "lib": ["dom", "es2018"], "baseUrl": "./" }, diff --git a/packages/angular/README.md b/packages/angular/README.md index a3e15a426196..c451b39ea9ef 100644 --- a/packages/angular/README.md +++ b/packages/angular/README.md @@ -17,11 +17,14 @@ ## Angular Version Compatibility -**Important**: This package is not compatible with Angular 16 or newer. Please use [`@sentry/angular-ivy`](https://github.com/getsentry/sentry-javascript/tree/master/packages/angular-ivy) instead. +**Important**: This package is not compatible with Angular 16 or newer. Please use +[`@sentry/angular-ivy`](https://github.com/getsentry/sentry-javascript/tree/master/packages/angular-ivy) instead. -If you're using Angular 12 or newer, we recommend using `@sentry/angular-ivy` for native support with Angular's rendering engine Ivy. +If you're using Angular 12 or newer, we recommend using `@sentry/angular-ivy` for native support with Angular's +rendering engine Ivy. -This SDK still officially supports Angular 10-15. If you are using an older version of Angular and experience problems with the Angular SDK, we recommend downgrading the SDK to version 6.x. +This SDK still officially supports Angular 10-15. If you are using an older version of Angular and experience problems +with the Angular SDK, we recommend downgrading the SDK to version 6.x. ## General @@ -80,26 +83,23 @@ see `ErrorHandlerOptions` interface in `src/errorhandler.ts`. ### Tracing -`@sentry/angular` exports a Trace Service, Directive and Decorators that leverage the tracing -features to add Angular-related spans to transactions. If tracing is not enabled, this functionality -will not work. The SDK's `TraceService` itself tracks route changes and durations, while directive and decorators are tracking -components initializations. +`@sentry/angular` exports a Trace Service, Directive and Decorators that leverage the tracing features to add +Angular-related spans to transactions. If tracing is not enabled, this functionality will not work. The SDK's +`TraceService` itself tracks route changes and durations, while directive and decorators are tracking components +initializations. #### Install Registering a Trace Service is a 3-step process. -1. Register and configure the `BrowserTracing` integration, including custom Angular routing - instrumentation: +1. Register and configure the `BrowserTracing` integration, including custom Angular routing instrumentation: ```javascript import { init, browserTracingIntegration } from '@sentry/angular'; init({ dsn: '__DSN__', - integrations: [ - browserTracingIntegration(), - ], + integrations: [browserTracingIntegration()], tracePropagationTargets: ['localhost', 'https://yourserver.io/api'], tracesSampleRate: 1, }); @@ -181,39 +181,40 @@ Then, inside your component's template (keep in mind that the directive's name a ``` -_TraceClassDecorator:_ used to track a duration between `OnInit` and `AfterViewInit` lifecycle hooks in components: +_TraceClass:_ used to track a duration between `OnInit` and `AfterViewInit` lifecycle hooks in components: ```javascript import { Component } from '@angular/core'; -import { TraceClassDecorator } from '@sentry/angular'; +import { TraceClass } from '@sentry/angular'; @Component({ selector: 'layout-header', templateUrl: './header.component.html', }) -@TraceClassDecorator() +@TraceClass() export class HeaderComponent { // ... } ``` -_TraceMethodDecorator:_ used to track a specific lifecycle hooks as point-in-time spans in components: +_TraceMethod:_ used to track a specific lifecycle hooks as point-in-time spans in components: ```javascript import { Component, OnInit } from '@angular/core'; -import { TraceMethodDecorator } from '@sentry/angular'; +import { TraceMethod } from '@sentry/angular'; @Component({ selector: 'app-footer', templateUrl: './footer.component.html', }) export class FooterComponent implements OnInit { - @TraceMethodDecorator() + @TraceMethod() ngOnInit() {} } ``` -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: +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'; @@ -223,12 +224,13 @@ import { init, startSpan } from '@sentry/angular'; import { AppModule } from './app/app.module'; // ... -startSpan({ - name: 'platform-browser-dynamic', - op: 'ui.angular.bootstrap' +startSpan( + { + name: 'platform-browser-dynamic', + op: 'ui.angular.bootstrap', }, async () => { await platformBrowserDynamic().bootstrapModule(AppModule); - } + }, ); ``` diff --git a/packages/angular/package.json b/packages/angular/package.json index 9c2de6ed8d18..e4c45d045923 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "main": "build/bundles/sentry-angular.umd.js", "module": "build/fesm2015/sentry-angular.js", diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index a2b1195c4e3c..fe4c1fd04c38 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -5,15 +5,9 @@ export * from '@sentry/browser'; export { init } from './sdk'; export { createErrorHandler, SentryErrorHandler } from './errorhandler'; export { - // eslint-disable-next-line deprecation/deprecation - getActiveTransaction, - // eslint-disable-next-line deprecation/deprecation - instrumentAngularRouting, // new name - // eslint-disable-next-line deprecation/deprecation - routingInstrumentation, // legacy name browserTracingIntegration, - TraceClassDecorator, - TraceMethodDecorator, + TraceClass, + TraceMethod, TraceDirective, TraceModule, TraceService, diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 6ad3ba46776d..59b9653e02ec 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-lines */ import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core'; import { Directive, Injectable, Input, NgModule } from '@angular/core'; import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router'; @@ -10,13 +9,16 @@ import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - WINDOW, browserTracingIntegration as originalBrowserTracingIntegration, + getActiveSpan, + getClient, getCurrentScope, + getRootSpan, + spanToJSON, startBrowserTracingNavigationSpan, + startInactiveSpan, } from '@sentry/browser'; -import { getActiveSpan, getClient, getRootSpan, spanToJSON, startInactiveSpan } from '@sentry/core'; -import type { Integration, Span, Transaction, TransactionContext } from '@sentry/types'; +import type { Integration, Span } from '@sentry/types'; import { logger, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/utils'; import type { Observable } from 'rxjs'; import { Subscription } from 'rxjs'; @@ -27,44 +29,6 @@ import { IS_DEBUG_BUILD } from './flags'; import { runOutsideAngular } from './zone'; let instrumentationInitialized: boolean; -let stashedStartTransaction: (context: TransactionContext) => Transaction | undefined; -let stashedStartTransactionOnLocationChange: boolean; - -let hooksBasedInstrumentation = false; - -/** - * Creates routing instrumentation for Angular Router. - * - * @deprecated Use `browserTracingIntegration()` instead, which includes Angular-specific instrumentation out of the box. - */ -export function routingInstrumentation( - customStartTransaction: (context: TransactionContext) => Transaction | undefined, - startTransactionOnPageLoad: boolean = true, - startTransactionOnLocationChange: boolean = true, -): void { - instrumentationInitialized = true; - stashedStartTransaction = customStartTransaction; - stashedStartTransactionOnLocationChange = startTransactionOnLocationChange; - - if (startTransactionOnPageLoad && WINDOW && WINDOW.location) { - customStartTransaction({ - name: WINDOW.location.pathname, - op: 'pageload', - origin: 'auto.pageload.angular', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', - }, - }); - } -} - -/** - * Creates routing instrumentation for Angular Router. - * - * @deprecated Use `browserTracingIntegration()` instead, which includes Angular-specific instrumentation out of the box. - */ -// eslint-disable-next-line deprecation/deprecation -export const instrumentAngularRouting = routingInstrumentation; /** * A custom browser tracing integration for Angular. @@ -78,7 +42,6 @@ export function browserTracingIntegration( // That way, the TraceService will not actually do anything, functionally disabling this. if (options.instrumentNavigation !== false) { instrumentationInitialized = true; - hooksBasedInstrumentation = true; } return originalBrowserTracingIntegration({ @@ -88,13 +51,16 @@ export function browserTracingIntegration( } /** - * Grabs active transaction off scope. - * - * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. + * This function is extracted to make unit testing easier. */ -export function getActiveTransaction(): Transaction | undefined { - // eslint-disable-next-line deprecation/deprecation - return getCurrentScope().getTransaction(); +export function _updateSpanAttributesForParametrizedUrl(route: string, span?: Span): void { + const attributes = (span && spanToJSON(span).data) || {}; + + if (span && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { + span.updateName(route); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, `auto.${spanToJSON(span).op}.angular`); + } } /** @@ -120,7 +86,7 @@ export class TraceService implements OnDestroy { const client = getClient(); const strippedUrl = stripUrlQueryAndFragment(navigationEvent.url); - if (client && hooksBasedInstrumentation) { + if (client) { // see comment in `_isPageloadOngoing` for rationale if (!this._isPageloadOngoing()) { startBrowserTracingNavigationSpan(client, { @@ -153,35 +119,6 @@ export class TraceService implements OnDestroy { return; } - - // eslint-disable-next-line deprecation/deprecation - let activeTransaction = getActiveTransaction(); - - if (!activeTransaction && stashedStartTransactionOnLocationChange) { - activeTransaction = stashedStartTransaction({ - name: strippedUrl, - op: 'navigation', - origin: 'auto.navigation.angular', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', - }, - }); - } - - if (activeTransaction) { - // eslint-disable-next-line deprecation/deprecation - this._routingSpan = activeTransaction.startChild({ - name: `${navigationEvent.url}`, - op: ANGULAR_ROUTING_OP, - origin: 'auto.ui.angular', - attributes: { - url: strippedUrl, - ...(navigationEvent.navigationTrigger && { - navigationTrigger: navigationEvent.navigationTrigger, - }), - }, - }); - } }), ); @@ -200,15 +137,14 @@ 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? - const attributes = (transaction && spanToJSON(transaction).data) || {}; - if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { - transaction.updateName(route); - transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, `auto.${spanToJSON(transaction).op}.angular`); + if (route) { + getCurrentScope().setTransactionName(route); } + + const activeSpan = getActiveSpan(); + const rootSpan = activeSpan && getRootSpan(activeSpan); + + _updateSpanAttributesForParametrizedUrl(route, rootSpan); }), ); @@ -274,7 +210,7 @@ export class TraceService implements OnDestroy { * - if `_pageloadOngoing` is already `false`, create a navigation root span * - if there's no active/pageload root span, create a navigation root span * - only if there's an ongoing pageload root span AND `_pageloadOngoing` is still `true, - * con't create a navigation root span + * don't create a navigation root span */ private _isPageloadOngoing(): boolean { if (!this._pageloadOngoing) { @@ -289,10 +225,6 @@ export class TraceService implements OnDestroy { } const rootSpan = getRootSpan(activeSpan); - if (!rootSpan) { - this._pageloadOngoing = false; - return false; - } this._pageloadOngoing = spanToJSON(rootSpan).op === 'pageload'; return this._pageloadOngoing; @@ -319,14 +251,11 @@ 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({ + if (getActiveSpan()) { + this._tracingSpan = startInactiveSpan({ name: `<${this.componentName}>`, op: ANGULAR_INIT_OP, - origin: 'auto.ui.angular.trace_directive', + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive' }, }); } } @@ -351,28 +280,33 @@ export class TraceDirective implements OnInit, AfterViewInit { }) export class TraceModule {} +interface TraceClassOptions { + /** + * Name of the class + */ + name?: string; +} + /** * Decorator function that can be used to capture initialization lifecycle of the whole component. */ -export function TraceClassDecorator(): ClassDecorator { +export function TraceClass(options?: TraceClassOptions): ClassDecorator { let tracingSpan: Span; /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type return target => { 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({ - name: `<${target.name}>`, - op: ANGULAR_INIT_OP, - origin: 'auto.ui.angular.trace_class_decorator', - }); - } + tracingSpan = startInactiveSpan({ + onlyIfParent: true, + name: `<${options && options.name ? options.name : 'unnamed'}>`, + op: ANGULAR_INIT_OP, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', + }, + }); + if (originalOnInit) { return originalOnInit.apply(this, args); } @@ -392,28 +326,34 @@ export function TraceClassDecorator(): ClassDecorator { /* eslint-enable @typescript-eslint/no-unsafe-member-access */ } +interface TraceMethodOptions { + /** + * Name of the method (is added to the tracing span) + */ + name?: string; +} + /** * Decorator function that can be used to capture a single lifecycle methods of the component. */ -export function TraceMethodDecorator(): MethodDecorator { - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/ban-types +export function TraceMethod(options?: TraceMethodOptions): MethodDecorator { + // eslint-disable-next-line @typescript-eslint/ban-types return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { const originalMethod = descriptor.value; // 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({ - name: `<${target.constructor.name}>`, - endTimestamp: now, - op: `${ANGULAR_OP}.${String(propertyKey)}`, - origin: 'auto.ui.angular.trace_method_decorator', - startTimestamp: now, - }); - } + + startInactiveSpan({ + onlyIfParent: true, + name: `<${options && options.name ? options.name : 'unnamed'}>`, + op: `${ANGULAR_OP}.${String(propertyKey)}`, + startTime: now, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + }, + }).end(now); + if (originalMethod) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return originalMethod.apply(this, args); diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index 5a935f178c71..f41b40750b81 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -1,30 +1,15 @@ -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'; -import { AppComponent, TestEnv } from './utils/index'; +import type { ActivatedRouteSnapshot } from '@angular/router'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SentrySpan, + spanToJSON, +} from '@sentry/core'; +import { TraceDirective, browserTracingIntegration, init } from '../src/index'; +import { _updateSpanAttributesForParametrizedUrl, getParameterizedRouteFromSnapshot } from '../src/tracing'; let transaction: any; -const defaultStartTransaction = (ctx: any) => { - transaction = { - ...ctx, - updateName: jest.fn(name => (transaction.name = name)), - setAttribute: jest.fn(), - toJSON: () => ({ - data: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', - ...ctx.data, - ...ctx.attributes, - }, - }), - }; - - return transaction; -}; - jest.mock('@sentry/browser', () => { const original = jest.requireActual('@sentry/browser'); return { @@ -39,27 +24,18 @@ jest.mock('@sentry/browser', () => { }; }); +describe('browserTracingIntegration', () => { + it('implements required hooks', () => { + const integration = browserTracingIntegration(); + expect(integration.name).toEqual('BrowserTracing'); + }); +}); + describe('Angular Tracing', () => { beforeEach(() => { transaction = undefined; }); - /* eslint-disable deprecation/deprecation */ - describe('instrumentAngularRouting', () => { - it('should attach the transaction source on the pageload transaction', () => { - const startTransaction = jest.fn(); - instrumentAngularRouting(startTransaction); - - expect(startTransaction).toHaveBeenCalledWith({ - name: '/', - op: 'pageload', - origin: 'auto.pageload.angular', - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, - }); - }); - }); - /* eslint-enable deprecation/deprecation */ - describe('getParameterizedRouteFromSnapshot', () => { it.each([ ['returns `/` if the route has no children', {}, '/'], @@ -113,240 +89,44 @@ describe('Angular Tracing', () => { }); describe('TraceService', () => { - it('does not change the transaction name if the source is something other than `url`', async () => { - const customStartTransaction = jest.fn((ctx: any) => { - transaction = { - ...ctx, - toJSON: () => ({ - data: { - ...ctx.data, - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', - }, - }), - metadata: ctx.metadata, - updateName: jest.fn(name => (transaction.name = name)), - setAttribute: jest.fn(), - }; - - return transaction; - }); - - const env = await TestEnv.setup({ - customStartTransaction, - routes: [ - { - path: '', - component: AppComponent, - }, - ], - }); - - const url = '/'; - - await env.navigateInAngular(url); - - expect(customStartTransaction).toHaveBeenCalledWith({ - name: url, - op: 'pageload', - origin: 'auto.pageload.angular', - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, - }); - - expect(transaction.updateName).toHaveBeenCalledTimes(0); - expect(transaction.name).toEqual(url); - expect(transaction.toJSON().data).toEqual({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }); - - env.destroy(); - }); - - it('re-assigns routing span on navigation start with active transaction.', async () => { - const customStartTransaction = jest.fn(defaultStartTransaction); - - const env = await TestEnv.setup({ - customStartTransaction, - }); - - const finishMock = jest.fn(); - transaction.startChild = jest.fn(() => ({ - end: finishMock, - })); - - await env.navigateInAngular('/'); - - expect(finishMock).toHaveBeenCalledTimes(1); + it('change the span name to route name if the the source is `url`', async () => { + init({ integrations: [browserTracingIntegration()] }); - env.destroy(); - }); - - it('finishes routing span on navigation end', async () => { - const customStartTransaction = jest.fn(defaultStartTransaction); - - const env = await TestEnv.setup({ - customStartTransaction, - }); - - const finishMock = jest.fn(); - transaction.startChild = jest.fn(() => ({ - end: finishMock, - })); - - await env.navigateInAngular('/'); - - expect(finishMock).toHaveBeenCalledTimes(1); - - env.destroy(); - }); - - it('finishes routing span on navigation error', async () => { - const customStartTransaction = jest.fn(defaultStartTransaction); - - const env = await TestEnv.setup({ - customStartTransaction, - routes: [ - { - path: '', - component: AppComponent, - }, - ], - useTraceService: true, - }); - - const finishMock = jest.fn(); - transaction.startChild = jest.fn(() => ({ - end: finishMock, - })); - - await env.navigateInAngular('/somewhere'); + const route = 'sample-route'; + const span = new SentrySpan({ name: 'initial-span-name' }); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); - expect(finishMock).toHaveBeenCalledTimes(1); + _updateSpanAttributesForParametrizedUrl(route, span); - env.destroy(); - }); - - it('finishes routing span on navigation cancel', async () => { - const customStartTransaction = jest.fn(defaultStartTransaction); - - class CanActivateGuard implements CanActivate { - canActivate(_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): boolean { - return false; - } - } - - const env = await TestEnv.setup({ - customStartTransaction, - routes: [ - { - path: 'cancel', - component: AppComponent, - canActivate: [CanActivateGuard], + expect(spanToJSON(span)).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.undefined.angular', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }, - ], - useTraceService: true, - additionalProviders: [{ provide: CanActivateGuard, useClass: CanActivateGuard }], - }); - - const finishMock = jest.fn(); - transaction.startChild = jest.fn(() => ({ - end: finishMock, - })); - - await env.navigateInAngular('/cancel'); - - expect(finishMock).toHaveBeenCalledTimes(1); - - env.destroy(); + description: route, + }), + ); }); - describe('URL parameterization', () => { - it.each([ - [ - 'handles the root URL correctly', - '/', - '/', - [ - { - path: '', - component: AppComponent, - }, - ], - ], - [ - 'does not alter static routes', - '/books', - '/books/', - [ - { - path: 'books', - component: AppComponent, - }, - ], - ], - [ - 'parameterizes IDs in the URL', - '/books/1/details', - '/books/:bookId/details/', - [ - { - path: 'books/:bookId/details', - component: AppComponent, - }, - ], - ], - [ - 'parameterizes multiple IDs in the URL', - '/org/sentry/projects/1234/events/04bc6846-4a1e-4af5-984a-003258f33e31', - '/org/:orgId/projects/:projId/events/:eventId/', - [ - { - path: 'org/:orgId/projects/:projId/events/:eventId', - component: AppComponent, - }, - ], - ], - [ - 'parameterizes URLs from route with child routes', - '/org/sentry/projects/1234/events/04bc6846-4a1e-4af5-984a-003258f33e31', - '/org/:orgId/projects/:projId/events/:eventId/', - [ - { - path: 'org/:orgId', - component: AppComponent, - children: [ - { - path: 'projects/:projId', - component: AppComponent, - children: [ - { - path: 'events/:eventId', - component: AppComponent, - }, - ], - }, - ], - }, - ], - ], - ])('%s and sets the source to `route`', async (_, url, result, routes) => { - const customStartTransaction = jest.fn(defaultStartTransaction); - const env = await TestEnv.setup({ - customStartTransaction, - routes, - startTransactionOnPageLoad: false, - }); + it('does not change the span name if the source is something other than `url`', async () => { + init({ integrations: [browserTracingIntegration()] }); - await env.navigateInAngular(url); + const route = 'sample-route'; + const span = new SentrySpan({ name: 'initial-span-name' }); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'sample-source'); - expect(customStartTransaction).toHaveBeenCalledWith({ - name: url, - op: 'navigation', - origin: 'auto.navigation.angular', - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, - }); - expect(transaction.updateName).toHaveBeenCalledWith(result); - expect(transaction.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + _updateSpanAttributesForParametrizedUrl(route, span); - env.destroy(); - }); + expect(spanToJSON(span)).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'sample-source', + }, + description: 'initial-span-name', + }), + ); }); }); @@ -355,196 +135,5 @@ describe('Angular Tracing', () => { const directive = new TraceDirective(); expect(directive).toBeTruthy(); }); - - it('should create a child tracingSpan on init', async () => { - const directive = new TraceDirective(); - const customStartTransaction = jest.fn(defaultStartTransaction); - - const env = await TestEnv.setup({ - components: [TraceDirective], - customStartTransaction, - useTraceService: false, - }); - - transaction.startChild = jest.fn(); - - directive.ngOnInit(); - - expect(transaction.startChild).toHaveBeenCalledWith({ - op: 'ui.angular.init', - origin: 'auto.ui.angular.trace_directive', - name: '', - }); - - env.destroy(); - }); - - it('should use component name as span name', async () => { - const directive = new TraceDirective(); - const finishMock = jest.fn(); - const customStartTransaction = jest.fn(defaultStartTransaction); - - const env = await TestEnv.setup({ - components: [TraceDirective], - customStartTransaction, - useTraceService: false, - }); - - transaction.startChild = jest.fn(() => ({ - end: finishMock, - })); - - directive.componentName = 'test-component'; - directive.ngOnInit(); - - expect(transaction.startChild).toHaveBeenCalledWith({ - op: 'ui.angular.init', - origin: 'auto.ui.angular.trace_directive', - name: '', - }); - - env.destroy(); - }); - - it('should finish tracingSpan after view init', async () => { - const directive = new TraceDirective(); - const finishMock = jest.fn(); - const customStartTransaction = jest.fn(defaultStartTransaction); - - const env = await TestEnv.setup({ - components: [TraceDirective], - customStartTransaction, - useTraceService: false, - }); - - transaction.startChild = jest.fn(() => ({ - end: finishMock, - })); - - directive.ngOnInit(); - directive.ngAfterViewInit(); - - expect(finishMock).toHaveBeenCalledTimes(1); - - env.destroy(); - }); - }); - - describe('TraceClassDecorator', () => { - const origNgOnInitMock = jest.fn(); - const origNgAfterViewInitMock = jest.fn(); - - @Component({ - selector: 'layout-header', - template: '', - }) - @TraceClassDecorator() - class DecoratedComponent { - public ngOnInit() { - origNgOnInitMock(); - } - - public ngAfterViewInit() { - origNgAfterViewInitMock(); - } - } - - it('Instruments `ngOnInit` and `ngAfterViewInit` methods of the decorated class', async () => { - const finishMock = jest.fn(); - const startChildMock = jest.fn(() => ({ - end: finishMock, - })); - - const customStartTransaction = jest.fn((ctx: any) => { - transaction = { - ...ctx, - startChild: startChildMock, - }; - - return transaction; - }); - - const env = await TestEnv.setup({ - customStartTransaction, - components: [DecoratedComponent], - defaultComponent: DecoratedComponent, - useTraceService: false, - }); - - expect(transaction.startChild).toHaveBeenCalledWith({ - name: '', - op: 'ui.angular.init', - origin: 'auto.ui.angular.trace_class_decorator', - }); - - expect(origNgOnInitMock).toHaveBeenCalledTimes(1); - expect(origNgAfterViewInitMock).toHaveBeenCalledTimes(1); - expect(finishMock).toHaveBeenCalledTimes(1); - - env.destroy(); - }); - }); - - describe('TraceMethodDecorator', () => { - const origNgOnInitMock = jest.fn(); - const origNgAfterViewInitMock = jest.fn(); - - @Component({ - selector: 'layout-header', - template: '', - }) - class DecoratedComponent { - @TraceMethodDecorator() - public ngOnInit() { - origNgOnInitMock(); - } - - @TraceMethodDecorator() - public ngAfterViewInit() { - origNgAfterViewInitMock(); - } - } - - it('Instruments `ngOnInit` and `ngAfterViewInit` methods of the decorated class', async () => { - const startChildMock = jest.fn(); - - const customStartTransaction = jest.fn((ctx: any) => { - transaction = { - ...ctx, - startChild: startChildMock, - }; - - return transaction; - }); - - const env = await TestEnv.setup({ - customStartTransaction, - components: [DecoratedComponent], - defaultComponent: DecoratedComponent, - useTraceService: false, - }); - - expect(transaction.startChild).toHaveBeenCalledTimes(2); - expect(transaction.startChild.mock.calls[0][0]).toEqual({ - name: '', - op: 'ui.angular.ngOnInit', - origin: 'auto.ui.angular.trace_method_decorator', - startTimestamp: expect.any(Number), - endTimestamp: expect.any(Number), - }); - - expect(transaction.startChild.mock.calls[1][0]).toEqual({ - name: '', - op: 'ui.angular.ngAfterViewInit', - origin: 'auto.ui.angular.trace_method_decorator', - startTimestamp: expect.any(Number), - endTimestamp: expect.any(Number), - }); - - expect(origNgOnInitMock).toHaveBeenCalledTimes(1); - expect(origNgAfterViewInitMock).toHaveBeenCalledTimes(1); - - env.destroy(); - }); }); }); diff --git a/packages/angular/test/utils/index.ts b/packages/angular/test/utils/index.ts deleted file mode 100644 index 390d7fbe14ac..000000000000 --- a/packages/angular/test/utils/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { Provider } from '@angular/core'; -import { Component, NgModule } from '@angular/core'; -import type { ComponentFixture } from '@angular/core/testing'; -import { TestBed } from '@angular/core/testing'; -import type { Routes } from '@angular/router'; -import { Router } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; -import type { Transaction } from '@sentry/types'; - -import { TraceService, instrumentAngularRouting } from '../../src'; - -@Component({ - template: '', -}) -export class AppComponent {} - -@NgModule({ - providers: [ - { - provide: TraceService, - deps: [Router], - }, - ], -}) -export class AppModule {} - -const defaultRoutes = [ - { - path: '', - component: AppComponent, - }, -]; - -export class TestEnv { - constructor( - public router: Router, - public fixture: ComponentFixture, - public traceService: TraceService | null, - ) { - fixture.detectChanges(); - } - - public static async setup(conf: { - routes?: Routes; - components?: any[]; - defaultComponent?: any; - customStartTransaction?: (context: any) => Transaction | undefined; - startTransactionOnPageLoad?: boolean; - startTransactionOnNavigation?: boolean; - useTraceService?: boolean; - additionalProviders?: Provider[]; - }): Promise { - // eslint-disable-next-line deprecation/deprecation - instrumentAngularRouting( - conf.customStartTransaction || jest.fn(), - conf.startTransactionOnPageLoad !== undefined ? conf.startTransactionOnPageLoad : true, - conf.startTransactionOnNavigation !== undefined ? conf.startTransactionOnNavigation : true, - ); - - const useTraceService = conf.useTraceService !== undefined ? conf.useTraceService : true; - const routes = conf.routes === undefined ? defaultRoutes : conf.routes; - - TestBed.configureTestingModule({ - imports: [AppModule, RouterTestingModule.withRoutes(routes)], - declarations: [...(conf.components || []), AppComponent], - providers: (useTraceService - ? [ - { - provide: TraceService, - deps: [Router], - }, - ...(conf.additionalProviders || []), - ] - : [] - ).concat(...(conf.additionalProviders || [])), - }); - - const router: Router = TestBed.inject(Router); - const traceService = useTraceService ? new TraceService(router) : null; - const fixture = TestBed.createComponent(conf.defaultComponent || AppComponent); - - return new TestEnv(router, fixture, traceService); - } - - public async navigateInAngular(url: string): Promise { - return new Promise(resolve => { - return this.fixture.ngZone?.run(() => { - void this.router - .navigateByUrl(url) - .then(() => { - this.fixture.detectChanges(); - resolve(); - }) - .catch(() => { - this.fixture.detectChanges(); - resolve(); - }); - }); - }); - } - - public destroy(): void { - if (this.traceService) { - this.traceService.ngOnDestroy(); - } - - jest.clearAllMocks(); - } -} diff --git a/packages/angular/tsconfig.ngc.json b/packages/angular/tsconfig.ngc.json index 9dd04ce14239..a7449189e22b 100644 --- a/packages/angular/tsconfig.ngc.json +++ b/packages/angular/tsconfig.ngc.json @@ -5,8 +5,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "target": "es2015", - "lib": ["dom", "es2015"], + "target": "es2017", + "lib": ["dom", "es2017"], "baseUrl": "./" }, "angularCompilerOptions": { diff --git a/packages/astro/README.md b/packages/astro/README.md index 696b5f948ab0..bbcf09f03720 100644 --- a/packages/astro/README.md +++ b/packages/astro/README.md @@ -12,12 +12,12 @@ ## Links - - [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/astro/) +- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/astro/) ## SDK Status -This SDK is in Beta and not yet fully stable. -If you have feedback or encounter any bugs, feel free to [open an issue](https://github.com/getsentry/sentry-javascript/issues/new/choose). +This SDK is in Beta and not yet fully stable. If you have feedback or encounter any bugs, feel free to +[open an issue](https://github.com/getsentry/sentry-javascript/issues/new/choose). ## General @@ -34,15 +34,15 @@ npx astro add @sentry/astro Add your DSN and source maps upload configuration: ```javascript -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: "your-sentry-project-slug", + project: 'your-sentry-project-slug', authToken: process.env.SENTRY_AUTH_TOKEN, }, }), @@ -50,7 +50,8 @@ export default defineConfig({ }); ``` -Follow [this guide](https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens) to create an auth token and add it to your environment variables: +Follow [this guide](https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens) to create an auth +token and add it to your environment variables: ```bash SENTRY_AUTH_TOKEN="your-token" @@ -58,14 +59,15 @@ SENTRY_AUTH_TOKEN="your-token" ### Server Instrumentation -For Astro apps configured for (hybrid) Server Side Rendering (SSR), the Sentry integration will automatically add middleware to your server to instrument incoming requests **if you're using Astro 3.5.2 or newer**. +For Astro apps configured for (hybrid) Server Side Rendering (SSR), the Sentry integration will automatically add +middleware to your server to instrument incoming requests **if you're using Astro 3.5.2 or newer**. If you're using Astro <3.5.2, complete the setup by adding the Sentry middleware to your `src/middleware.js` file: ```javascript // src/middleware.js -import { sequence } from "astro:middleware"; -import * as Sentry from "@sentry/astro"; +import { sequence } from 'astro:middleware'; +import * as Sentry from '@sentry/astro'; export const onRequest = sequence( Sentry.handleRequest(), @@ -74,6 +76,7 @@ export const onRequest = sequence( ``` The Sentry middleware enhances the data collected by Sentry on the server side by: + - Enabeling distributed tracing between client and server - Collecting performance spans for incoming requests - Enhancing captured errors with additional information @@ -83,26 +86,25 @@ The Sentry middleware enhances the data collected by Sentry on the server side b You can opt out of using the automatic sentry server instrumentation in your `astro.config.mjs` file: ```javascript -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__', autoInstrumentation: { requestHandler: false, - } + }, }), ], }); ``` - ## Configuration Check out our docs for configuring your SDK setup: -* [Getting Started](https://docs.sentry.io/platforms/javascript/guides/astro/) -* [Manual Setup and Configuration](https://docs.sentry.io/platforms/javascript/guides/astro/manual-setup/) -* [Source Maps Upload](https://docs.sentry.io/platforms/javascript/guides/astro/sourcemaps/) +- [Getting Started](https://docs.sentry.io/platforms/javascript/guides/astro/) +- [Manual Setup and Configuration](https://docs.sentry.io/platforms/javascript/guides/astro/manual-setup/) +- [Source Maps Upload](https://docs.sentry.io/platforms/javascript/guides/astro/sourcemaps/) diff --git a/packages/astro/package.json b/packages/astro/package.json index cea8347b19f6..f0a16b8fec1a 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -51,14 +51,13 @@ "dependencies": { "@sentry/browser": "8.0.0-alpha.2", "@sentry/core": "8.0.0-alpha.2", - "@sentry/node-experimental": "8.0.0-alpha.2", + "@sentry/node": "8.0.0-alpha.2", "@sentry/types": "8.0.0-alpha.2", "@sentry/utils": "8.0.0-alpha.2", "@sentry/vite-plugin": "^2.14.2" }, "devDependencies": { "astro": "^3.5.0", - "rollup": "^3.20.2", "vite": "4.0.5" }, "scripts": { diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 39012cc546b3..1cc715c8345a 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -1,5 +1,5 @@ // Node SDK exports -// Unfortunately, we cannot `export * from '@sentry/node-experimental'` because in prod builds, +// Unfortunately, we cannot `export * from '@sentry/node'` because in prod builds, // Vite puts these exports into a `default` property (Sentry.default) rather than // on the top - level namespace. @@ -37,7 +37,6 @@ export { setHttpStatus, withScope, withIsolationScope, - autoDiscoverNodePerformanceMonitoringIntegrations, makeNodeTransport, getDefaultIntegrations, defaultStackParser, @@ -47,7 +46,6 @@ export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData, - Integrations, consoleIntegration, onUncaughtExceptionIntegration, onUnhandledRejectionIntegration, @@ -59,13 +57,14 @@ export { functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration, - Handlers, setMeasurement, getActiveSpan, + getRootSpan, startSpan, startInactiveSpan, startSpanManual, withActiveSpan, + getSpanDescendants, continueTrace, cron, parameterize, @@ -73,10 +72,25 @@ export { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/node-experimental'; + expressIntegration, + expressErrorHandler, + setupExpressErrorHandler, + fastifyIntegration, + graphqlIntegration, + mongoIntegration, + mongooseIntegration, + mysqlIntegration, + mysql2Integration, + nestIntegration, + postgresIntegration, + prismaIntegration, + hapiIntegration, + setupHapiErrorHandler, + spotlightIntegration, +} from '@sentry/node'; // We can still leave this for the carrier init and type exports -export * from '@sentry/node-experimental'; +export * from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index 11ea06bae7c1..dad62a288d53 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -13,10 +13,6 @@ import sentryAstro from './index.server'; /** Initializes Sentry Astro SDK */ export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): void; -// We only export server Integrations for now, until the exports are removed from Svelte and Node SDKs. -// Necessary to avoid type collision. -export declare const Integrations: typeof serverSdk.Integrations; - export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; @@ -26,5 +22,14 @@ export declare const defaultStackParser: StackParser; export declare function close(timeout?: number | undefined): PromiseLike; export declare function flush(timeout?: number | undefined): PromiseLike; +// eslint-disable-next-line deprecation/deprecation +export declare const makeMain: typeof clientSdk.makeMain; +// eslint-disable-next-line deprecation/deprecation +export declare const getCurrentHub: typeof clientSdk.getCurrentHub; +export declare const getClient: typeof clientSdk.getClient; +export declare const continueTrace: typeof clientSdk.continueTrace; + +export declare const Span: clientSdk.Span; + export declare const metrics: typeof clientSdk.metrics & typeof serverSdk.metrics; export default sentryAstro; diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index eedbd9407adf..15e48cb49f12 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -91,7 +91,7 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { // @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-experimental', '@sentry/node'], + noExternal: ['@sentry/astro', '@sentry/node'], }, }, }); diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index b5c0e599754a..ec0dbb11d09a 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,13 +1,15 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setHttpStatus } from '@sentry/core'; import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, continueTrace, getActiveSpan, getClient, getCurrentScope, + setHttpStatus, startSpan, withIsolationScope, -} from '@sentry/node-experimental'; +} from '@sentry/node'; import type { Client, Scope, Span, SpanAttributes } from '@sentry/types'; import { addNonEnumerableProperty, objectify, stripUrlQueryAndFragment } from '@sentry/utils'; import type { APIContext, MiddlewareResponseHandler } from 'astro'; diff --git a/packages/astro/src/server/sdk.ts b/packages/astro/src/server/sdk.ts index d7b4983a7bc3..a34a33cad31d 100644 --- a/packages/astro/src/server/sdk.ts +++ b/packages/astro/src/server/sdk.ts @@ -1,15 +1,22 @@ import { applySdkMetadata } from '@sentry/core'; -import type { NodeOptions } from '@sentry/node-experimental'; -import { init as initNodeSdk, setTag } from '@sentry/node-experimental'; +import type { NodeOptions } from '@sentry/node'; +import { getDefaultIntegrations } from '@sentry/node'; +import { init as initNodeSdk, setTag } from '@sentry/node'; /** * * @param options */ export function init(options: NodeOptions): void { - applySdkMetadata(options, 'astro', ['astro', 'node']); + const opts = { + ...options, + // TODO v8: For now, we disable the Prisma integration, because that has weird esm-cjs interop issues + // We should figure these out and fix these before v8 goes stable. + defaultIntegrations: getDefaultIntegrations(options).filter(integration => integration.name !== 'Prisma'), + }; + applySdkMetadata(opts, 'astro', ['astro', 'node']); - initNodeSdk(options); + initNodeSdk(opts); setTag('runtime', 'node'); } diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index a344cd326bb6..23078dec65f5 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -1,11 +1,15 @@ import type { BrowserClient } from '@sentry/browser'; -import { getActiveSpan } from '@sentry/browser'; -import { browserTracingIntegration } from '@sentry/browser'; +import { + browserTracingIntegration, + getActiveSpan, + getCurrentScope, + getGlobalScope, + getIsolationScope, +} from '@sentry/browser'; import * as SentryBrowser from '@sentry/browser'; import { SDK_VERSION, getClient } from '@sentry/browser'; import { vi } from 'vitest'; -import { getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; import { init } from '../../../astro/src/client/sdk'; const browserInit = vi.spyOn(SentryBrowser, 'init'); diff --git a/packages/astro/test/server/meta.test.ts b/packages/astro/test/server/meta.test.ts index 37506cb118b7..8b65beaa4eaf 100644 --- a/packages/astro/test/server/meta.test.ts +++ b/packages/astro/test/server/meta.test.ts @@ -5,7 +5,7 @@ import { vi } from 'vitest'; import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta'; -const TRACE_FLAG_SAMPLED = 0x1; +const TRACE_FLAG_SAMPLED = 1; const mockedSpan = new SentrySpan({ traceId: '12345678901234567890123456789012', diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index cd5caec43c48..a678fcceaee6 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,5 +1,5 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import * as SentryNode from '@sentry/node-experimental'; +import * as SentryNode from '@sentry/node'; import type { Client, Span } from '@sentry/types'; import { vi } from 'vitest'; diff --git a/packages/astro/test/server/sdk.test.ts b/packages/astro/test/server/sdk.test.ts index b1dc3821c88f..1b042ee5331a 100644 --- a/packages/astro/test/server/sdk.test.ts +++ b/packages/astro/test/server/sdk.test.ts @@ -1,5 +1,5 @@ -import * as SentryNode from '@sentry/node-experimental'; -import { SDK_VERSION } from '@sentry/node-experimental'; +import * as SentryNode from '@sentry/node'; +import { SDK_VERSION } from '@sentry/node'; import { vi } from 'vitest'; import { init } from '../../src/server/sdk'; diff --git a/packages/serverless/.eslintrc.js b/packages/aws-serverless/.eslintrc.js similarity index 100% rename from packages/serverless/.eslintrc.js rename to packages/aws-serverless/.eslintrc.js diff --git a/packages/serverless/LICENSE b/packages/aws-serverless/LICENSE similarity index 100% rename from packages/serverless/LICENSE rename to packages/aws-serverless/LICENSE diff --git a/packages/aws-serverless/README.md b/packages/aws-serverless/README.md new file mode 100644 index 000000000000..5a1ea8a1cc00 --- /dev/null +++ b/packages/aws-serverless/README.md @@ -0,0 +1,67 @@ +

+ + Sentry + +

+ +# Official Sentry SDK for Serverless environments + +## Links + +- [Official SDK Docs](https://docs.sentry.io/) +- [TypeDoc](http://getsentry.github.io/sentry-javascript/) + +## General + +This package is a wrapper around `@sentry/node`, with added functionality related to various Serverless solutions. All +methods available in `@sentry/node` can be imported from `@sentry/aws-serverless`. + +Currently supported environment: + +### AWS Lambda + +To use this SDK, call `Sentry.init(options)` at the very beginning of your JavaScript file. + +```javascript +import * as Sentry from '@sentry/aws-serverless'; + +Sentry.init({ + dsn: '__DSN__', + // ... +}); + +// async (recommended) +exports.handler = Sentry.wrapHandler(async (event, context) => { + throw new Error('oh, hello there!'); +}); + +// sync +exports.handler = Sentry.wrapHandler((event, context, callback) => { + throw new Error('oh, hello there!'); +}); +``` + +If you also want to trace performance of all the incoming requests and also outgoing AWS service requests, just set the +`tracesSampleRate` option. + +```javascript +import * as Sentry from '@sentry/aws-serverless'; + +Sentry.AWSLambda.init({ + dsn: '__DSN__', + tracesSampleRate: 1.0, +}); +``` + +#### Integrate Sentry using internal extension + +Another and much simpler way to integrate Sentry to your AWS Lambda function is to add an official layer. + +1. Choose Layers -> Add Layer. +2. Specify an ARN: `arn:aws:lambda:us-west-1:TODO:layer:TODO:VERSION`. +3. Go to Environment variables and add: + - `NODE_OPTIONS`: `-r @sentry/aws-serverless/build/npm/cjs/awslambda-auto`. + - `SENTRY_DSN`: `your dsn`. + - `SENTRY_TRACES_SAMPLE_RATE`: a number between 0 and 1 representing the chance a transaction is sent to Sentry. For + more information, see + [docs](https://docs.sentry.io/platforms/node/guides/aws-lambda/configuration/options/#tracesSampleRate). diff --git a/packages/serverless/jest.config.js b/packages/aws-serverless/jest.config.js similarity index 100% rename from packages/serverless/jest.config.js rename to packages/aws-serverless/jest.config.js diff --git a/packages/serverless/package.json b/packages/aws-serverless/package.json similarity index 85% rename from packages/serverless/package.json rename to packages/aws-serverless/package.json index f5324520cf73..431609842da1 100644 --- a/packages/serverless/package.json +++ b/packages/aws-serverless/package.json @@ -1,23 +1,30 @@ { - "name": "@sentry/serverless", + "name": "@sentry/aws-serverless", "version": "8.0.0-alpha.2", - "description": "Official Sentry SDK for various serverless solutions", + "description": "Official Sentry SDK for AWS Lambda and AWS Serverless Environments", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "files": [ "cjs", - "esm", "types", "types-ts3.8" ], "main": "build/npm/cjs/index.js", - "module": "build/npm/esm/index.js", "types": "build/npm/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "require": { + "types": "./build/npm/types/index.d.ts", + "default": "./build/npm/cjs/index.js" + } + } + }, "typesVersions": { "<4.9": { "build/npm/types/index.d.ts": [ @@ -30,21 +37,16 @@ }, "dependencies": { "@sentry/core": "8.0.0-alpha.2", - "@sentry/node-experimental": "8.0.0-alpha.2", + "@sentry/node": "8.0.0-alpha.2", "@sentry/types": "8.0.0-alpha.2", "@sentry/utils": "8.0.0-alpha.2", "@types/aws-lambda": "^8.10.62", "@types/express": "^4.17.14" }, "devDependencies": { - "@google-cloud/bigquery": "^5.3.0", - "@google-cloud/common": "^3.4.1", - "@google-cloud/functions-framework": "^1.7.1", - "@google-cloud/pubsub": "^2.5.0", "@types/node": "^14.6.4", "aws-sdk": "^2.765.0", "find-up": "^5.0.0", - "google-gax": "^2.9.0", "nock": "^13.0.4", "npm-packlist": "^2.1.4" }, diff --git a/packages/serverless/rollup.aws.config.mjs b/packages/aws-serverless/rollup.aws.config.mjs similarity index 74% rename from packages/serverless/rollup.aws.config.mjs rename to packages/aws-serverless/rollup.aws.config.mjs index 5d9883a3f9f7..22656f397140 100644 --- a/packages/serverless/rollup.aws.config.mjs +++ b/packages/aws-serverless/rollup.aws.config.mjs @@ -6,13 +6,12 @@ export default [ makeBaseBundleConfig({ // this automatically sets it to be CJS bundleType: 'node', - entrypoints: ['src/index.awslambda.ts'], - jsVersion: 'es6', - licenseTitle: '@sentry/serverless', + entrypoints: ['src/index.ts'], + licenseTitle: '@sentry/aws-serverless', outputFileBase: () => 'index', packageSpecificConfig: { output: { - dir: 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/build/npm/cjs', + dir: 'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs', sourcemap: false, }, }, @@ -22,10 +21,6 @@ export default [ // it to be `index.js` in the build script, since it's standing in for the index file of the npm package. { variants: ['.min.js'] }, ), - - // This builds a wrapper file, which our lambda layer integration automatically sets up to run as soon as node - // launches (via the `NODE_OPTIONS="-r @sentry/serverless/dist/awslambda-auto"` variable). Note the inclusion in this - // path of the legacy `dist` folder; for backwards compatibility, in the build script we'll copy the file there. makeBaseNPMConfig({ entrypoints: ['src/awslambda-auto.ts'], packageSpecificConfig: { @@ -33,7 +28,7 @@ export default [ // and the directory structure is different than normal, so we have to do it ourselves. output: { format: 'cjs', - dir: 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/build/npm/cjs', + dir: 'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs', sourcemap: false, }, // We only want `awslambda-auto.js`, not the modules that it imports, because they're all included in the bundle diff --git a/packages/serverless/rollup.npm.config.mjs b/packages/aws-serverless/rollup.npm.config.mjs similarity index 96% rename from packages/serverless/rollup.npm.config.mjs rename to packages/aws-serverless/rollup.npm.config.mjs index b51a3bdafdb5..ff28359cfeed 100644 --- a/packages/serverless/rollup.npm.config.mjs +++ b/packages/aws-serverless/rollup.npm.config.mjs @@ -9,4 +9,5 @@ export default makeNPMConfigVariants( // packages with bundles have a different build directory structure hasBundles: true, }), + { emitEsm: false }, ); diff --git a/packages/serverless/scripts/buildLambdaLayer.ts b/packages/aws-serverless/scripts/buildLambdaLayer.ts similarity index 84% rename from packages/serverless/scripts/buildLambdaLayer.ts rename to packages/aws-serverless/scripts/buildLambdaLayer.ts index 3522ff021a33..310cfe606ca0 100644 --- a/packages/serverless/scripts/buildLambdaLayer.ts +++ b/packages/aws-serverless/scripts/buildLambdaLayer.ts @@ -20,27 +20,27 @@ async function buildLambdaLayer(): Promise { // We build a minified bundle, but it's standing in for the regular `index.js` file listed in `package.json`'s `main` // property, so we have to rename it so it's findable. fs.renameSync( - 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/build/npm/cjs/index.min.js', - 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/build/npm/cjs/index.js', + 'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/index.min.js', + 'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/index.js', ); // We're creating a bundle for the SDK, but still using it in a Node context, so we need to copy in `package.json`, // purely for its `main` property. console.log('Copying `package.json` into lambda layer.'); - fs.copyFileSync('package.json', 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/package.json'); + fs.copyFileSync('package.json', 'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/package.json'); // The layer also includes `awslambda-auto.js`, a helper file which calls `Sentry.init()` and wraps the lambda // handler. It gets run when Node is launched inside the lambda, using the environment variable // - // `NODE_OPTIONS="-r @sentry/serverless/dist/awslambda-auto"`. + // `NODE_OPTIONS="-r @sentry/aws-serverless/dist/awslambda-auto"`. // // (The`-r` is what runs the script on startup.) The `dist` directory is no longer where we emit our built code, so // for backwards compatibility, we create a symlink. console.log('Creating symlink for `awslambda-auto.js` in legacy `dist` directory.'); - fsForceMkdirSync('build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/dist'); + fsForceMkdirSync('build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/dist'); fs.symlinkSync( '../build/npm/cjs/awslambda-auto.js', - 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/dist/awslambda-auto.js', + 'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/dist/awslambda-auto.js', ); const zipFilename = `sentry-node-serverless-${version}.zip`; diff --git a/packages/serverless/src/awslambda-auto.ts b/packages/aws-serverless/src/awslambda-auto.ts similarity index 65% rename from packages/serverless/src/awslambda-auto.ts rename to packages/aws-serverless/src/awslambda-auto.ts index ac048cde5aed..6287b35e8651 100644 --- a/packages/serverless/src/awslambda-auto.ts +++ b/packages/aws-serverless/src/awslambda-auto.ts @@ -1,4 +1,4 @@ -import * as Sentry from './index'; +import { init, tryPatchHandler } from './awslambda'; const lambdaTaskRoot = process.env.LAMBDA_TASK_ROOT; if (lambdaTaskRoot) { @@ -7,11 +7,9 @@ if (lambdaTaskRoot) { throw Error(`LAMBDA_TASK_ROOT is non-empty(${lambdaTaskRoot}) but _HANDLER is not set`); } - Sentry.AWSLambda.init({ - _invokedByLambdaLayer: true, - }); + init(); - Sentry.AWSLambda.tryPatchHandler(lambdaTaskRoot, handlerString); + tryPatchHandler(lambdaTaskRoot, handlerString); } else { throw Error('LAMBDA_TASK_ROOT environment variable is not set'); } diff --git a/packages/serverless/src/awslambda.ts b/packages/aws-serverless/src/awslambda.ts similarity index 95% rename from packages/serverless/src/awslambda.ts rename to packages/aws-serverless/src/awslambda.ts index 5a6fcea389fa..b918494fdb90 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/aws-serverless/src/awslambda.ts @@ -2,9 +2,9 @@ import { existsSync } from 'fs'; import { hostname } from 'os'; import { basename, resolve } from 'path'; import { types } from 'util'; -import type { NodeOptions } from '@sentry/node-experimental'; -import { SDK_VERSION } from '@sentry/node-experimental'; +import type { NodeOptions } from '@sentry/node'; import { + SDK_VERSION, captureException, captureMessage, continueTrace, @@ -14,7 +14,7 @@ import { init as initNode, startSpanManual, withScope, -} from '@sentry/node-experimental'; +} from '@sentry/node'; import type { Integration, Options, Scope, SdkMetadata, Span } from '@sentry/types'; import { isString, logger } from '@sentry/utils'; import type { Context, Handler } from 'aws-lambda'; @@ -26,8 +26,6 @@ import { awsServicesIntegration } from './awsservices'; import { DEBUG_BUILD } from './debug-build'; import { markEventUnhandled } from './utils'; -export * from '@sentry/node-experimental'; - const { isPromise } = types; // https://www.npmjs.com/package/aws-lambda-consumer @@ -70,20 +68,12 @@ export function getDefaultIntegrations(options: Options): Integration[] { return [...getNodeDefaultIntegrations(options), awsServicesIntegration({ optional: true })]; } -interface AWSLambdaOptions extends NodeOptions { - /** - * Internal field that is set to `true` when init() is called by the Sentry AWS Lambda layer. - * - */ - _invokedByLambdaLayer?: boolean; -} - /** * Initializes the Sentry AWS Lambda SDK. * * @param options Configuration options for the SDK, @see {@link AWSLambdaOptions}. */ -export function init(options: AWSLambdaOptions = {}): void { +export function init(options: NodeOptions = {}): void { const opts = { _metadata: {} as SdkMetadata, defaultIntegrations: getDefaultIntegrations(options), @@ -91,11 +81,11 @@ export function init(options: AWSLambdaOptions = {}): void { }; opts._metadata.sdk = opts._metadata.sdk || { - name: 'sentry.javascript.serverless', + name: 'sentry.javascript.aws-serverless', integrations: ['AWSLambda'], packages: [ { - name: 'npm:@sentry/serverless', + name: 'npm:@sentry/aws-serverless', version: SDK_VERSION, }, ], diff --git a/packages/serverless/src/awsservices.ts b/packages/aws-serverless/src/awsservices.ts similarity index 83% rename from packages/serverless/src/awsservices.ts rename to packages/aws-serverless/src/awsservices.ts index 33e4816ac836..e8a46c6fda00 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/aws-serverless/src/awsservices.ts @@ -1,6 +1,6 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; -import { getClient, startInactiveSpan } from '@sentry/node-experimental'; -import type { Client, Integration, IntegrationClass, IntegrationFn, Span } from '@sentry/types'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration } from '@sentry/core'; +import { getClient, startInactiveSpan } from '@sentry/node'; +import type { Client, IntegrationFn, Span } from '@sentry/types'; import { fill } from '@sentry/utils'; // 'aws-sdk/global' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. @@ -41,21 +41,10 @@ const _awsServicesIntegration = ((options: { optional?: boolean } = {}) => { }; }) satisfies IntegrationFn; -export const awsServicesIntegration = defineIntegration(_awsServicesIntegration); - /** * AWS Service Request Tracking. - * - * @deprecated Use `awsServicesIntegration()` instead. */ -// eslint-disable-next-line deprecation/deprecation -export const AWSServices = convertIntegrationFnToClass( - INTEGRATION_NAME, - awsServicesIntegration, -) as IntegrationClass; - -// eslint-disable-next-line deprecation/deprecation -export type AWSServices = typeof AWSServices; +export const awsServicesIntegration = defineIntegration(_awsServicesIntegration); /** * Patches AWS SDK request to create `http.client` spans. @@ -64,10 +53,10 @@ function wrapMakeRequest( orig: MakeRequestFunction, ): MakeRequestFunction { return function (this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback) { - let span: Span | undefined; const req = orig.call(this, operation, params); if (SETUP_CLIENTS.has(getClient() as Client)) { + let span: Span | undefined; req.on('afterBuild', () => { span = startInactiveSpan({ name: describe(this, operation, params), @@ -79,9 +68,7 @@ function wrapMakeRequest( }); }); req.on('complete', () => { - if (span) { - span.end(); - } + span?.end(); }); } diff --git a/packages/feedback/src/debug-build.ts b/packages/aws-serverless/src/debug-build.ts similarity index 100% rename from packages/feedback/src/debug-build.ts rename to packages/aws-serverless/src/debug-build.ts diff --git a/packages/serverless/src/index.ts b/packages/aws-serverless/src/index.ts similarity index 58% rename from packages/serverless/src/index.ts rename to packages/aws-serverless/src/index.ts index 5e153ae7f4c5..d439c016faa7 100644 --- a/packages/serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -1,103 +1,94 @@ -// https://medium.com/unsplash/named-namespace-imports-7345212bbffb -import * as AWSLambda from './awslambda'; -import * as GCPFunction from './gcpfunction'; -export { AWSLambda, GCPFunction }; - -// eslint-disable-next-line deprecation/deprecation -export { AWSServices, awsServicesIntegration } from './awsservices'; - -// TODO(v8): We have to explicitly export these because of the namespace exports -// above. This is because just doing `export * from '@sentry/node-experimental'` will not -// work with Node native esm while we also have namespace exports in a package. -// What we should do is get rid of the namespace exports. export { - Hub, - SDK_VERSION, - Scope, - addBreadcrumb, - // eslint-disable-next-line deprecation/deprecation - addGlobalEventProcessor, addEventProcessor, + addBreadcrumb, addIntegration, - autoDiscoverNodePerformanceMonitoringIntegrations, - captureEvent, captureException, + captureEvent, captureMessage, captureCheckIn, + startSession, + captureSession, + endSession, withMonitor, createTransport, // eslint-disable-next-line deprecation/deprecation - getActiveTransaction, - // eslint-disable-next-line deprecation/deprecation getCurrentHub, getClient, isInitialized, getCurrentScope, getGlobalScope, getIsolationScope, - getSpanStatusFromHttpCode, - setHttpStatus, - // eslint-disable-next-line deprecation/deprecation - makeMain, + Hub, setCurrentClient, + Scope, + SDK_VERSION, setContext, setExtra, setExtras, setTag, setTags, setUser, - // eslint-disable-next-line deprecation/deprecation - startTransaction, + getSpanStatusFromHttpCode, + setHttpStatus, withScope, withIsolationScope, - NodeClient, makeNodeTransport, - close, - getDefaultIntegrations, + NodeClient, defaultStackParser, flush, + close, getSentryRelease, - init, - DEFAULT_USER_INCLUDES, addRequestDataToEvent, + DEFAULT_USER_INCLUDES, extractRequestData, - Handlers, - // eslint-disable-next-line deprecation/deprecation - Integrations, - setMeasurement, - getActiveSpan, - startSpan, - startInactiveSpan, - startSpanManual, - withActiveSpan, - continueTrace, - parameterize, - requestDataIntegration, - linkedErrorsIntegration, - inboundFiltersIntegration, - functionToStringIntegration, createGetModuleFromFilename, - metrics, + anrIntegration, consoleIntegration, + httpIntegration, + nativeNodeFetchIntegration, onUncaughtExceptionIntegration, onUnhandledRejectionIntegration, modulesIntegration, contextLinesIntegration, nodeContextIntegration, localVariablesIntegration, - anrIntegration, - hapiIntegration, - httpIntegration, - nativeNodeFetchintegration, - spotlightIntegration, + requestDataIntegration, + functionToStringIntegration, + inboundFiltersIntegration, + linkedErrorsIntegration, + setMeasurement, + getActiveSpan, + startSpan, + startInactiveSpan, + startSpanManual, + withActiveSpan, + getRootSpan, + getSpanDescendants, + continueTrace, + getAutoPerformanceIntegrations, + cron, + metrics, + parameterize, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, - startSession, - captureSession, - endSession, -} from '@sentry/node-experimental'; + expressIntegration, + expressErrorHandler, + setupExpressErrorHandler, + fastifyIntegration, + graphqlIntegration, + mongoIntegration, + mongooseIntegration, + mysqlIntegration, + mysql2Integration, + nestIntegration, + postgresIntegration, + prismaIntegration, + hapiIntegration, + setupHapiErrorHandler, + spotlightIntegration, +} from '@sentry/node'; export { captureConsoleIntegration, @@ -107,3 +98,8 @@ export { rewriteFramesIntegration, sessionTimingIntegration, } from '@sentry/core'; + +export { getDefaultIntegrations, init, tryPatchHandler, wrapHandler } from './awslambda'; +export type { WrapperOptions } from './awslambda'; + +export { awsServicesIntegration } from './awsservices'; diff --git a/packages/aws-serverless/src/utils.ts b/packages/aws-serverless/src/utils.ts new file mode 100644 index 000000000000..259388bb193c --- /dev/null +++ b/packages/aws-serverless/src/utils.ts @@ -0,0 +1,14 @@ +import type { Scope } from '@sentry/types'; +import { addExceptionMechanism } from '@sentry/utils'; + +/** + * Marks an event as unhandled by adding a span processor to the passed scope. + */ +export function markEventUnhandled(scope: Scope): Scope { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { handled: false }); + return event; + }); + + return scope; +} diff --git a/packages/serverless/test/__mocks__/dns.ts b/packages/aws-serverless/test/__mocks__/dns.ts similarity index 100% rename from packages/serverless/test/__mocks__/dns.ts rename to packages/aws-serverless/test/__mocks__/dns.ts diff --git a/packages/serverless/test/awslambda.test.ts b/packages/aws-serverless/test/awslambda.test.ts similarity index 98% rename from packages/serverless/test/awslambda.test.ts rename to packages/aws-serverless/test/awslambda.test.ts index 964c760f0ca3..bc37f8dfc0a8 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/aws-serverless/test/awslambda.test.ts @@ -20,8 +20,8 @@ const mockScope = { addEventProcessor: jest.fn(), }; -jest.mock('@sentry/node-experimental', () => { - const original = jest.requireActual('@sentry/node-experimental'); +jest.mock('@sentry/node', () => { + const original = jest.requireActual('@sentry/node'); return { ...original, init: (options: unknown) => { @@ -517,11 +517,11 @@ describe('AWSLambda', () => { expect.objectContaining({ _metadata: { sdk: { - name: 'sentry.javascript.serverless', + name: 'sentry.javascript.aws-serverless', integrations: ['AWSLambda'], packages: [ { - name: 'npm:@sentry/serverless', + name: 'npm:@sentry/aws-serverless', version: expect.any(String), }, ], diff --git a/packages/serverless/test/awsservices.test.ts b/packages/aws-serverless/test/awsservices.test.ts similarity index 97% rename from packages/serverless/test/awsservices.test.ts rename to packages/aws-serverless/test/awsservices.test.ts index c48abb173e16..3170f9056ec0 100644 --- a/packages/serverless/test/awsservices.test.ts +++ b/packages/aws-serverless/test/awsservices.test.ts @@ -1,4 +1,4 @@ -import { NodeClient, createTransport, setCurrentClient } from '@sentry/node-experimental'; +import { NodeClient, createTransport, setCurrentClient } from '@sentry/node'; import * as AWS from 'aws-sdk'; import * as nock from 'nock'; @@ -8,9 +8,9 @@ import { awsServicesIntegration } from '../src/awsservices'; const mockSpanEnd = jest.fn(); const mockStartInactiveSpan = jest.fn(spanArgs => ({ ...spanArgs })); -jest.mock('@sentry/node-experimental', () => { +jest.mock('@sentry/node', () => { return { - ...jest.requireActual('@sentry/node-experimental'), + ...jest.requireActual('@sentry/node'), startInactiveSpan: (ctx: unknown) => { mockStartInactiveSpan(ctx); return { end: mockSpanEnd }; diff --git a/packages/serverless/tsconfig.json b/packages/aws-serverless/tsconfig.json similarity index 100% rename from packages/serverless/tsconfig.json rename to packages/aws-serverless/tsconfig.json diff --git a/packages/serverless/tsconfig.test.json b/packages/aws-serverless/tsconfig.test.json similarity index 100% rename from packages/serverless/tsconfig.test.json rename to packages/aws-serverless/tsconfig.test.json diff --git a/packages/serverless/tsconfig.types.json b/packages/aws-serverless/tsconfig.types.json similarity index 100% rename from packages/serverless/tsconfig.types.json rename to packages/aws-serverless/tsconfig.types.json diff --git a/packages/browser/package.json b/packages/browser/package.json index 876e3c45889d..1a6a79c0cc41 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "files": [ "cjs", @@ -18,6 +18,19 @@ "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", "types": "build/npm/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/npm/types/index.d.ts", + "default": "./build/npm/esm/index.js" + }, + "require": { + "types": "./build/npm.types/index.d.ts", + "default": "./build/npm/cjs/index.js" + } + } + }, "typesVersions": { "<4.9": { "build/npm/types/index.d.ts": [ @@ -64,9 +77,7 @@ "scripts": { "build": "run-p build:transpile build:bundle build:types", "build:dev": "yarn build", - "build:bundle": "run-p build:bundle:es5 build:bundle:es6", - "build:bundle:es5": "JS_VERSION=es5 rollup -c rollup.bundle.config.mjs", - "build:bundle:es6": "JS_VERSION=es6 rollup -c rollup.bundle.config.mjs", + "build:bundle": "rollup -c rollup.bundle.config.mjs", "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", @@ -81,10 +92,7 @@ "clean": "rimraf build coverage .rpt2_cache sentry-browser-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", - "validate:es5": "es-check es5 'build/bundles/*.es5*.js'", - "size:check": "run-p size:check:es5 size:check:es6", - "size:check:es5": "cat build/bundles/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES5: \",$1,\"kB\";}'", - "size:check:es6": "cat build/bundles/bundle.es6.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES6: \",$1,\"kB\";}'", + "size:check": "cat build/bundles/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES2017: \",$1,\"kB\";}'", "test": "yarn test:unit", "test:unit": "jest", "test:integration": "test/integration/run.js", diff --git a/packages/browser/rollup.bundle.config.mjs b/packages/browser/rollup.bundle.config.mjs index d262f1a1bc51..7ebddd4e2f04 100644 --- a/packages/browser/rollup.bundle.config.mjs +++ b/packages/browser/rollup.bundle.config.mjs @@ -2,12 +2,6 @@ import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal const builds = []; -const targets = process.env.JS_VERSION ? [process.env.JS_VERSION] : ['es5', 'es6']; - -if (targets.some(target => target !== 'es5' && target !== 'es6')) { - throw new Error('JS_VERSION must be either "es5" or "es6"'); -} - const browserPluggableIntegrationFiles = ['contextlines', 'httpclient', 'reportingobserver']; const corePluggableIntegrationFiles = [ @@ -19,90 +13,77 @@ const corePluggableIntegrationFiles = [ 'sessiontiming', ]; -targets.forEach(jsVersion => { - const baseBundleConfig = makeBaseBundleConfig({ - bundleType: 'standalone', - entrypoints: ['src/index.bundle.ts'], - jsVersion, - licenseTitle: '@sentry/browser', - outputFileBase: () => `bundles/bundle${jsVersion === 'es5' ? '.es5' : ''}`, - }); - - const tracingBaseBundleConfig = makeBaseBundleConfig({ - bundleType: 'standalone', - entrypoints: ['src/index.bundle.tracing.ts'], - jsVersion, - licenseTitle: '@sentry/browser (Performance Monitoring)', - outputFileBase: () => `bundles/bundle.tracing${jsVersion === 'es5' ? '.es5' : ''}`, +browserPluggableIntegrationFiles.forEach(integrationName => { + const integrationsBundleConfig = makeBaseBundleConfig({ + bundleType: 'addon', + entrypoints: [`src/integrations/${integrationName}.ts`], + licenseTitle: `@sentry/browser - ${integrationName}`, + outputFileBase: () => `bundles/${integrationName}`, }); - browserPluggableIntegrationFiles.forEach(integrationName => { - const integrationsBundleConfig = makeBaseBundleConfig({ - bundleType: 'addon', - entrypoints: [`src/integrations/${integrationName}.ts`], - jsVersion, - licenseTitle: `@sentry/browser - ${integrationName}`, - outputFileBase: () => `bundles/${integrationName}${jsVersion === 'es5' ? '.es5' : ''}`, - }); + builds.push(...makeBundleConfigVariants(integrationsBundleConfig)); +}); - builds.push(...makeBundleConfigVariants(integrationsBundleConfig)); +corePluggableIntegrationFiles.forEach(integrationName => { + const integrationsBundleConfig = makeBaseBundleConfig({ + bundleType: 'addon', + entrypoints: [`src/integrations-bundle/index.${integrationName}.ts`], + licenseTitle: `@sentry/browser - ${integrationName}`, + outputFileBase: () => `bundles/${integrationName}`, }); - corePluggableIntegrationFiles.forEach(integrationName => { - const integrationsBundleConfig = makeBaseBundleConfig({ - bundleType: 'addon', - entrypoints: [`src/integrations-bundle/index.${integrationName}.ts`], - jsVersion, - licenseTitle: `@sentry/browser - ${integrationName}`, - outputFileBase: () => `bundles/${integrationName}${jsVersion === 'es5' ? '.es5' : ''}`, - }); + builds.push(...makeBundleConfigVariants(integrationsBundleConfig)); +}); - builds.push(...makeBundleConfigVariants(integrationsBundleConfig)); - }); +const baseBundleConfig = makeBaseBundleConfig({ + bundleType: 'standalone', + entrypoints: ['src/index.bundle.ts'], + licenseTitle: '@sentry/browser', + outputFileBase: () => 'bundles/bundle', +}); - builds.push(...makeBundleConfigVariants(baseBundleConfig), ...makeBundleConfigVariants(tracingBaseBundleConfig)); +const tracingBaseBundleConfig = makeBaseBundleConfig({ + bundleType: 'standalone', + entrypoints: ['src/index.bundle.tracing.ts'], + licenseTitle: '@sentry/browser (Performance Monitoring)', + outputFileBase: () => 'bundles/bundle.tracing', }); -if (targets.includes('es6')) { - // Replay/Feedback bundles only available for es6 - const replayBaseBundleConfig = makeBaseBundleConfig({ - bundleType: 'standalone', - entrypoints: ['src/index.bundle.replay.ts'], - jsVersion: 'es6', - licenseTitle: '@sentry/browser & @sentry/replay', - outputFileBase: () => 'bundles/bundle.replay', - }); +const replayBaseBundleConfig = makeBaseBundleConfig({ + bundleType: 'standalone', + entrypoints: ['src/index.bundle.replay.ts'], + licenseTitle: '@sentry/browser & @sentry/replay', + outputFileBase: () => 'bundles/bundle.replay', +}); - const feedbackBaseBundleConfig = makeBaseBundleConfig({ - bundleType: 'standalone', - entrypoints: ['src/index.bundle.feedback.ts'], - jsVersion: 'es6', - licenseTitle: '@sentry/browser & @sentry/feedback', - outputFileBase: () => 'bundles/bundle.feedback', - }); +const feedbackBaseBundleConfig = makeBaseBundleConfig({ + bundleType: 'standalone', + entrypoints: ['src/index.bundle.feedback.ts'], + licenseTitle: '@sentry/browser & @sentry/feedback', + outputFileBase: () => 'bundles/bundle.feedback', +}); - const tracingReplayBaseBundleConfig = makeBaseBundleConfig({ - bundleType: 'standalone', - entrypoints: ['src/index.bundle.tracing.replay.ts'], - jsVersion: 'es6', - licenseTitle: '@sentry/browser (Performance Monitoring and Replay)', - outputFileBase: () => 'bundles/bundle.tracing.replay', - }); +const tracingReplayBaseBundleConfig = makeBaseBundleConfig({ + bundleType: 'standalone', + entrypoints: ['src/index.bundle.tracing.replay.ts'], + licenseTitle: '@sentry/browser (Performance Monitoring and Replay)', + outputFileBase: () => 'bundles/bundle.tracing.replay', +}); - const tracingReplayFeedbackBaseBundleConfig = makeBaseBundleConfig({ - bundleType: 'standalone', - entrypoints: ['src/index.bundle.tracing.replay.feedback.ts'], - jsVersion: 'es6', - licenseTitle: '@sentry/browser (Performance Monitoring, Replay, and Feedback)', - outputFileBase: () => 'bundles/bundle.tracing.replay.feedback', - }); +const tracingReplayFeedbackBaseBundleConfig = makeBaseBundleConfig({ + bundleType: 'standalone', + entrypoints: ['src/index.bundle.tracing.replay.feedback.ts'], + licenseTitle: '@sentry/browser (Performance Monitoring, Replay, and Feedback)', + outputFileBase: () => 'bundles/bundle.tracing.replay.feedback', +}); - builds.push( - ...makeBundleConfigVariants(replayBaseBundleConfig), - ...makeBundleConfigVariants(feedbackBaseBundleConfig), - ...makeBundleConfigVariants(tracingReplayBaseBundleConfig), - ...makeBundleConfigVariants(tracingReplayFeedbackBaseBundleConfig), - ); -} +builds.push( + ...makeBundleConfigVariants(baseBundleConfig), + ...makeBundleConfigVariants(tracingBaseBundleConfig), + ...makeBundleConfigVariants(replayBaseBundleConfig), + ...makeBundleConfigVariants(feedbackBaseBundleConfig), + ...makeBundleConfigVariants(tracingReplayBaseBundleConfig), + ...makeBundleConfigVariants(tracingReplayFeedbackBaseBundleConfig), +); export default builds; diff --git a/packages/browser/rollup.npm.config.mjs b/packages/browser/rollup.npm.config.mjs index 6d09adefc859..2edfdefdc4da 100644 --- a/packages/browser/rollup.npm.config.mjs +++ b/packages/browser/rollup.npm.config.mjs @@ -4,5 +4,13 @@ export default makeNPMConfigVariants( makeBaseNPMConfig({ // packages with bundles have a different build directory structure hasBundles: true, + packageSpecificConfig: { + output: { + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + // set preserveModules to false because we want to bundle everything into one file. + preserveModules: false, + }, + }, }), ); diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index 2956006b0ac7..b39baee2a3fe 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -106,10 +106,11 @@ export function parseStackFrames( // reliably in other circumstances. const stacktrace = ex.stacktrace || ex.stack || ''; - const popSize = getPopSize(ex); + const skipLines = getSkipFirstStackStringLines(ex); + const framesToPop = getPopFirstTopFrames(ex); try { - return stackParser(stacktrace, popSize); + return stackParser(stacktrace, skipLines, framesToPop); } catch (e) { // no-empty } @@ -120,15 +121,30 @@ export function parseStackFrames( // Based on our own mapping pattern - https://github.com/getsentry/sentry/blob/9f08305e09866c8bd6d0c24f5b0aabdd7dd6c59c/src/sentry/lang/javascript/errormapping.py#L83-L108 const reactMinifiedRegexp = /Minified React error #\d+;/i; -function getPopSize(ex: Error & { framesToPop?: number }): number { - if (ex) { - if (typeof ex.framesToPop === 'number') { - return ex.framesToPop; - } +/** + * Certain known React errors contain links that would be falsely + * parsed as frames. This function check for these errors and + * returns number of the stack string lines to skip. + */ +function getSkipFirstStackStringLines(ex: Error): number { + if (ex && reactMinifiedRegexp.test(ex.message)) { + return 1; + } - if (reactMinifiedRegexp.test(ex.message)) { - return 1; - } + return 0; +} + +/** + * If error has `framesToPop` property, it means that the + * creator tells us the first x frames will be useless + * and should be discarded. Typically error from wrapper function + * which don't point to the actual location in the developer's code. + * + * Example: https://github.com/zertosh/invariant/blob/master/invariant.js#L46 + */ +function getPopFirstTopFrames(ex: Error & { framesToPop?: unknown }): number { + if (typeof ex.framesToPop === 'number') { + return ex.framesToPop; } return 0; diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 643235a2ec36..a1599f4d7f77 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -43,8 +43,6 @@ export { makeMain, setCurrentClient, Scope, - // eslint-disable-next-line deprecation/deprecation - startTransaction, continueTrace, SDK_VERSION, setContext, diff --git a/packages/browser/src/index.bundle.feedback.ts b/packages/browser/src/index.bundle.feedback.ts index 7cb0501e1a92..887cc2e864ac 100644 --- a/packages/browser/src/index.bundle.feedback.ts +++ b/packages/browser/src/index.bundle.feedback.ts @@ -1,27 +1,16 @@ // This is exported so the loader does not fail when switching off Replay/Tracing -import { Feedback, feedbackIntegration } from '@sentry-internal/feedback'; +import { feedbackIntegration } from '@sentry-internal/feedback'; import { - ReplayShim, addTracingExtensionsShim, browserTracingIntegrationShim, replayIntegrationShim, } from '@sentry-internal/integration-shims'; -import * as Sentry from './index.bundle.base'; - -// TODO (v8): Remove this as it was only needed for backwards compatibility -// eslint-disable-next-line deprecation/deprecation -Sentry.Integrations.Replay = ReplayShim; - export * from './index.bundle.base'; export { browserTracingIntegrationShim as browserTracingIntegration, addTracingExtensionsShim as addTracingExtensions, - // eslint-disable-next-line deprecation/deprecation - ReplayShim as Replay, replayIntegrationShim as replayIntegration, - // eslint-disable-next-line deprecation/deprecation - Feedback, feedbackIntegration, }; // Note: We do not export a shim for `Span` here, as that is quite complex and would blow up the bundle diff --git a/packages/browser/src/index.bundle.replay.ts b/packages/browser/src/index.bundle.replay.ts index 71baed234eba..29bf0b320dea 100644 --- a/packages/browser/src/index.bundle.replay.ts +++ b/packages/browser/src/index.bundle.replay.ts @@ -1,27 +1,16 @@ // This is exported so the loader does not fail when switching off Replay/Tracing import { - FeedbackShim, addTracingExtensionsShim, browserTracingIntegrationShim, feedbackIntegrationShim, } from '@sentry-internal/integration-shims'; -import { Replay, replayIntegration } from '@sentry/replay'; - -import * as Sentry from './index.bundle.base'; - -// TODO (v8): Remove this as it was only needed for backwards compatibility -// eslint-disable-next-line deprecation/deprecation -Sentry.Integrations.Replay = Replay; +import { replayIntegration } from '@sentry/replay'; export * from './index.bundle.base'; export { browserTracingIntegrationShim as browserTracingIntegration, addTracingExtensionsShim as addTracingExtensions, - // eslint-disable-next-line deprecation/deprecation - Replay, replayIntegration, - // eslint-disable-next-line deprecation/deprecation - FeedbackShim as Feedback, feedbackIntegrationShim as feedbackIntegration, }; // Note: We do not export a shim for `Span` here, as that is quite complex and would blow up the bundle diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index 2ccc69a8456f..1fe669f2393f 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -1,35 +1,21 @@ -import { Feedback, feedbackIntegration } from '@sentry-internal/feedback'; +import { feedbackIntegration } from '@sentry-internal/feedback'; import { browserTracingIntegration } from '@sentry-internal/tracing'; import { addTracingExtensions } from '@sentry/core'; -import { Replay, replayIntegration } from '@sentry/replay'; - -import * as Sentry from './index.bundle.base'; - -// TODO (v8): Remove this as it was only needed for backwards compatibility -// We want replay to be available under Sentry.Replay, to be consistent -// with the NPM package version. -// eslint-disable-next-line deprecation/deprecation -Sentry.Integrations.Replay = Replay; +import { replayIntegration } from '@sentry/replay'; // We are patching the global object with our hub extension methods addTracingExtensions(); export { getActiveSpan, + getRootSpan, startSpan, startInactiveSpan, startSpanManual, withActiveSpan, + getSpanDescendants, } from '@sentry/core'; -export { - // eslint-disable-next-line deprecation/deprecation - Feedback, - // eslint-disable-next-line deprecation/deprecation - Replay, - feedbackIntegration, - replayIntegration, - browserTracingIntegration, - addTracingExtensions, -}; +export { feedbackIntegration, replayIntegration, browserTracingIntegration, addTracingExtensions }; + export * from './index.bundle.base'; diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 8c7ac5bb917f..7b09054540b8 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -1,35 +1,26 @@ -import { FeedbackShim, feedbackIntegrationShim } from '@sentry-internal/integration-shims'; +import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; import { browserTracingIntegration } from '@sentry-internal/tracing'; import { addTracingExtensions } from '@sentry/core'; -import { Replay, replayIntegration } from '@sentry/replay'; - -import * as Sentry from './index.bundle.base'; - -// TODO (v8): Remove this as it was only needed for backwards compatibility -// We want replay to be available under Sentry.Replay, to be consistent -// with the NPM package version. -// eslint-disable-next-line deprecation/deprecation -Sentry.Integrations.Replay = Replay; +import { replayIntegration } from '@sentry/replay'; // We are patching the global object with our hub extension methods addTracingExtensions(); export { getActiveSpan, + getRootSpan, startSpan, startInactiveSpan, startSpanManual, withActiveSpan, + getSpanDescendants, } from '@sentry/core'; export { - // eslint-disable-next-line deprecation/deprecation - FeedbackShim as Feedback, - // eslint-disable-next-line deprecation/deprecation - Replay, replayIntegration, feedbackIntegrationShim as feedbackIntegration, browserTracingIntegration, addTracingExtensions, }; + export * from './index.bundle.base'; diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index c457db356d67..23c424fd7337 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -1,40 +1,26 @@ // This is exported so the loader does not fail when switching off Replay -import { - FeedbackShim, - ReplayShim, - feedbackIntegrationShim, - replayIntegrationShim, -} from '@sentry-internal/integration-shims'; +import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; import { browserTracingIntegration } from '@sentry-internal/tracing'; import { addTracingExtensions } from '@sentry/core'; -import * as Sentry from './index.bundle.base'; - -// TODO(v8): Remove this as it was only needed for backwards compatibility -// We want replay to be available under Sentry.Replay, to be consistent -// with the NPM package version. -// eslint-disable-next-line deprecation/deprecation -Sentry.Integrations.Replay = ReplayShim; - // We are patching the global object with our hub extension methods addTracingExtensions(); export { getActiveSpan, + getRootSpan, startSpan, startInactiveSpan, startSpanManual, withActiveSpan, + getSpanDescendants, } from '@sentry/core'; export { - // eslint-disable-next-line deprecation/deprecation - FeedbackShim as Feedback, - // eslint-disable-next-line deprecation/deprecation - ReplayShim as Replay, feedbackIntegrationShim as feedbackIntegration, replayIntegrationShim as replayIntegration, browserTracingIntegration, addTracingExtensions, }; + export * from './index.bundle.base'; diff --git a/packages/browser/src/index.bundle.ts b/packages/browser/src/index.bundle.ts index b113bd56d142..b27672414b20 100644 --- a/packages/browser/src/index.bundle.ts +++ b/packages/browser/src/index.bundle.ts @@ -1,26 +1,14 @@ // This is exported so the loader does not fail when switching off Replay/Tracing import { - FeedbackShim, - ReplayShim, addTracingExtensionsShim, browserTracingIntegrationShim, feedbackIntegrationShim, replayIntegrationShim, } from '@sentry-internal/integration-shims'; -import * as Sentry from './index.bundle.base'; - -// TODO (v8): Remove this as it was only needed for backwards compatibility -// eslint-disable-next-line deprecation/deprecation -Sentry.Integrations.Replay = ReplayShim; - export * from './index.bundle.base'; export { addTracingExtensionsShim as addTracingExtensions, - // eslint-disable-next-line deprecation/deprecation - ReplayShim as Replay, - // eslint-disable-next-line deprecation/deprecation - FeedbackShim as Feedback, browserTracingIntegrationShim as browserTracingIntegration, feedbackIntegrationShim as feedbackIntegration, replayIntegrationShim as replayIntegration, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index c6270e521188..f75b1e90e0b8 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -30,8 +30,6 @@ export { } from '@sentry/core'; export { - // eslint-disable-next-line deprecation/deprecation - Replay, replayIntegration, getReplay, } from '@sentry/replay'; @@ -47,15 +45,9 @@ export type { ReplaySpanFrameEvent, } from '@sentry/replay'; -export { - // eslint-disable-next-line deprecation/deprecation - ReplayCanvas, - replayCanvasIntegration, -} from '@sentry-internal/replay-canvas'; +export { replayCanvasIntegration } from '@sentry-internal/replay-canvas'; export { - // eslint-disable-next-line deprecation/deprecation - Feedback, feedbackIntegration, sendFeedback, } from '@sentry-internal/feedback'; @@ -71,10 +63,12 @@ export type { RequestInstrumentationOptions } from '@sentry-internal/tracing'; export { addTracingExtensions, getActiveSpan, + getRootSpan, startSpan, startInactiveSpan, startSpanManual, withActiveSpan, + getSpanDescendants, setMeasurement, // eslint-disable-next-line deprecation/deprecation getActiveTransaction, diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts index 5fa59dd3585d..0db88cfe355c 100644 --- a/packages/browser/src/integrations/httpcontext.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -1,13 +1,13 @@ -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; -import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; - +import { defineIntegration } from '@sentry/core'; import { WINDOW } from '../helpers'; -const INTEGRATION_NAME = 'HttpContext'; - -const _httpContextIntegration = (() => { +/** + * Collects information about HTTP request headers and + * attaches them to the event. + */ +export const httpContextIntegration = defineIntegration(() => { return { - name: INTEGRATION_NAME, + name: 'HttpContext', preprocessEvent(event) { // if none of the information we want exists, don't bother if (!WINDOW.navigator && !WINDOW.location && !WINDOW.document) { @@ -29,15 +29,4 @@ const _httpContextIntegration = (() => { event.request = request; }, }; -}) satisfies IntegrationFn; - -export const httpContextIntegration = defineIntegration(_httpContextIntegration); - -/** - * HttpContext integration collects information about HTTP request headers. - * @deprecated Use `httpContextIntegration()` instead. - */ -// eslint-disable-next-line deprecation/deprecation -export const HttpContext = convertIntegrationFnToClass(INTEGRATION_NAME, httpContextIntegration) as IntegrationClass< - Integration & { preprocessEvent: (event: Event) => void } ->; +}); diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 5b6137e10027..af88bd8d91e6 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -1,18 +1,17 @@ -import { defineIntegration, getCurrentScope } from '@sentry/core'; -import type { EventEnvelope, IntegrationFn, Transaction } from '@sentry/types'; +import { defineIntegration, getActiveSpan, getRootSpan } from '@sentry/core'; +import type { EventEnvelope, IntegrationFn, Span } from '@sentry/types'; import type { Profile } from '@sentry/types/src/profiling'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; -import { startProfileForTransaction } from './startProfileForTransaction'; +import { startProfileForSpan } from './startProfileForSpan'; import type { ProfiledEvent } from './utils'; +import { isAutomatedPageLoadSpan, shouldProfileSpan } from './utils'; import { addProfilesToEnvelope, createProfilingEvent, findProfiledTransactionsFromEnvelope, getActiveProfilesCount, - isAutomatedPageLoadTransaction, - shouldProfileTransaction, takeProfileFromGlobalCache, } from './utils'; @@ -21,22 +20,19 @@ const INTEGRATION_NAME = 'BrowserProfiling'; const _browserProfilingIntegration = (() => { return { name: INTEGRATION_NAME, - // TODO v8: Remove this setup(client) { - const scope = getCurrentScope(); + const activeSpan = getActiveSpan(); + const rootSpan = activeSpan && getRootSpan(activeSpan); - // eslint-disable-next-line deprecation/deprecation - const transaction = scope.getTransaction(); - - if (transaction && isAutomatedPageLoadTransaction(transaction)) { - if (shouldProfileTransaction(transaction)) { - startProfileForTransaction(transaction); + if (rootSpan && isAutomatedPageLoadSpan(rootSpan)) { + if (shouldProfileSpan(rootSpan)) { + startProfileForSpan(rootSpan); } } - client.on('startTransaction', (transaction: Transaction) => { - if (shouldProfileTransaction(transaction)) { - startProfileForTransaction(transaction); + client.on('spanStart', (span: Span) => { + if (span === getRootSpan(span) && shouldProfileSpan(span)) { + startProfileForSpan(span); } }); @@ -59,23 +55,23 @@ const _browserProfilingIntegration = (() => { const start_timestamp = context && context['profile'] && context['profile']['start_timestamp']; if (typeof profile_id !== 'string') { - DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context'); + DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a span without a profile context'); continue; } if (!profile_id) { - DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context'); + DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a span without a profile context'); continue; } - // Remove the profile from the transaction context before sending, relay will take care of the rest. + // Remove the profile from the span context before sending, relay will take care of the rest. if (context && context['profile']) { delete context.profile; } const profile = takeProfileFromGlobalCache(profile_id); if (!profile) { - DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); + DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for span: ${profile_id}`); continue; } diff --git a/packages/browser/src/profiling/startProfileForTransaction.ts b/packages/browser/src/profiling/startProfileForSpan.ts similarity index 54% rename from packages/browser/src/profiling/startProfileForTransaction.ts rename to packages/browser/src/profiling/startProfileForSpan.ts index 7d62cd8b1c46..9dcdb81a9e8e 100644 --- a/packages/browser/src/profiling/startProfileForTransaction.ts +++ b/packages/browser/src/profiling/startProfileForSpan.ts @@ -1,140 +1,132 @@ -import { spanToJSON } from '@sentry/core'; -import type { Transaction } from '@sentry/types'; +import { getCurrentScope, spanToJSON } from '@sentry/core'; +import type { Span } from '@sentry/types'; import { logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; import type { JSSelfProfile } from './jsSelfProfiling'; -import { - MAX_PROFILE_DURATION_MS, - addProfileToGlobalCache, - isAutomatedPageLoadTransaction, - startJSSelfProfile, -} from './utils'; +import { MAX_PROFILE_DURATION_MS, addProfileToGlobalCache, isAutomatedPageLoadSpan, startJSSelfProfile } from './utils'; /** * Wraps startTransaction and stopTransaction with profiling related logic. * startProfileForTransaction is called after the call to startTransaction in order to avoid our own code from * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction. */ -export function startProfileForTransaction(transaction: Transaction): Transaction { +export function startProfileForSpan(span: Span): void { // Start the profiler and get the profiler instance. let startTimestamp: number | undefined; - if (isAutomatedPageLoadTransaction(transaction)) { + if (isAutomatedPageLoadSpan(span)) { startTimestamp = timestampInSeconds() * 1000; } const profiler = startJSSelfProfile(); - // We failed to construct the profiler, fallback to original transaction. + // We failed to construct the profiler, so we skip. // No need to log anything as this has already been logged in startProfile. if (!profiler) { - return transaction; + return; } if (DEBUG_BUILD) { - logger.log(`[Profiling] started profiling transaction: ${spanToJSON(transaction).description}`); + logger.log(`[Profiling] started profiling span: ${spanToJSON(span).description}`); } - // We create "unique" transaction names to avoid concurrent transactions with same names - // from being ignored by the profiler. From here on, only this transaction name should be used when + // We create "unique" span names to avoid concurrent spans with same names + // from being ignored by the profiler. From here on, only this span name should be used when // calling the profiler methods. Note: we log the original name to the user to avoid confusion. const profileId = uuid4(); // A couple of important things to note here: // `CpuProfilerBindings.stopProfiling` will be scheduled to run in 30seconds in order to exceed max profile duration. - // Whichever of the two (transaction.finish/timeout) is first to run, the profiling will be stopped and the gathered profile - // will be processed when the original transaction is finished. Since onProfileHandler can be invoked multiple times in the - // event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler + // Whichever of the two (span.finish/timeout) is first to run, the profiling will be stopped and the gathered profile + // will be processed when the original span is finished. Since onProfileHandler can be invoked multiple times in the + // event of an error or user mistake (calling span.finish multiple times), it is important that the behavior of onProfileHandler // is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler. // After the original finish method is called, the event will be reported through the integration and delegated to transport. const processedProfile: JSSelfProfile | null = null; + getCurrentScope().setContext('profile', { + profile_id: profileId, + start_timestamp: startTimestamp, + }); + /** * Idempotent handler for profile stop */ - async function onProfileHandler(): Promise { - // Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times. - if (!transaction) { - return null; + async function onProfileHandler(): Promise { + // Check if the profile exists and return it the behavior has to be idempotent as users may call span.finish multiple times. + if (!span) { + return; } // Satisfy the type checker, but profiler will always be defined here. if (!profiler) { - return null; + return; } if (processedProfile) { if (DEBUG_BUILD) { - logger.log('[Profiling] profile for:', spanToJSON(transaction).description, 'already exists, returning early'); + logger.log('[Profiling] profile for:', spanToJSON(span).description, 'already exists, returning early'); } - return null; + return; } return profiler .stop() - .then((profile: JSSelfProfile): null => { + .then((profile: JSSelfProfile): void => { if (maxDurationTimeoutID) { WINDOW.clearTimeout(maxDurationTimeoutID); maxDurationTimeoutID = undefined; } if (DEBUG_BUILD) { - logger.log(`[Profiling] stopped profiling of transaction: ${spanToJSON(transaction).description}`); + logger.log(`[Profiling] stopped profiling of span: ${spanToJSON(span).description}`); } - // In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile. + // In case of an overlapping span, stopProfiling may return null and silently ignore the overlapping profile. if (!profile) { if (DEBUG_BUILD) { logger.log( - `[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', + `[Profiling] profiler returned null profile for: ${spanToJSON(span).description}`, + 'this may indicate an overlapping span or a call to stopProfiling with a profile title that was never started', ); } - return null; + return; } addProfileToGlobalCache(profileId, profile); - return null; }) .catch(error => { if (DEBUG_BUILD) { logger.log('[Profiling] error while stopping profiler:', error); } - return null; }); } // Enqueue a timeout to prevent profiles from running over max duration. let maxDurationTimeoutID: number | undefined = WINDOW.setTimeout(() => { if (DEBUG_BUILD) { - logger.log( - '[Profiling] max profile duration elapsed, stopping profiling for:', - spanToJSON(transaction).description, - ); + logger.log('[Profiling] max profile duration elapsed, stopping profiling for:', spanToJSON(span).description); } - // If the timeout exceeds, we want to stop profiling, but not finish the transaction + // If the timeout exceeds, we want to stop profiling, but not finish the span // eslint-disable-next-line @typescript-eslint/no-floating-promises onProfileHandler(); }, MAX_PROFILE_DURATION_MS); // We need to reference the original end call to avoid creating an infinite loop - const originalEnd = transaction.end.bind(transaction); + const originalEnd = span.end.bind(span); /** - * Wraps startTransaction and stopTransaction with profiling related logic. - * startProfiling is called after the call to startTransaction in order to avoid our own code from - * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction. + * Wraps span `end()` with profiling related logic. + * startProfiling is called after the call to spanStart in order to avoid our own code from + * being profiled. Because of that same reason, stopProfiling is called before the call to spanEnd. */ - function profilingWrappedTransactionEnd(): Transaction { - if (!transaction) { + function profilingWrappedSpanEnd(): Span { + if (!span) { return originalEnd(); } // onProfileHandler should always return the same profile even if this is called multiple times. // 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(); }, () => { @@ -143,9 +135,8 @@ export function startProfileForTransaction(transaction: Transaction): Transactio }, ); - return transaction; + return span; } - transaction.end = profilingWrappedTransactionEnd; - return transaction; + span.end = profilingWrappedSpanEnd; } diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index f4f700a2ab6f..46ae0c07442a 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import { DEFAULT_ENVIRONMENT, getClient, spanToJSON } from '@sentry/core'; -import type { DebugImage, Envelope, Event, EventEnvelope, StackFrame, StackParser, Transaction } from '@sentry/types'; +import type { DebugImage, Envelope, Event, EventEnvelope, Span, StackFrame, StackParser } from '@sentry/types'; import type { Profile, ThreadCpuProfile } from '@sentry/types/src/profiling'; import { GLOBAL_OBJ, browserPerformanceTimeOrigin, forEachEnvelopeItem, logger, uuid4 } from '@sentry/utils'; @@ -201,8 +201,8 @@ export function isProfiledTransactionEvent(event: Event): event is ProfiledEvent /** * */ -export function isAutomatedPageLoadTransaction(transaction: Transaction): boolean { - return spanToJSON(transaction).op === 'pageload'; +export function isAutomatedPageLoadSpan(span: Span): boolean { + return spanToJSON(span).op === 'pageload'; } /** @@ -506,7 +506,7 @@ export function startJSSelfProfile(): JSSelfProfiler | undefined { /** * Determine if a profile should be profiled. */ -export function shouldProfileTransaction(transaction: Transaction): boolean { +export function shouldProfileSpan(span: Span): boolean { // If constructor failed once, it will always fail, so we can early return. if (PROFILING_CONSTRUCTOR_FAILED) { if (DEBUG_BUILD) { @@ -515,7 +515,7 @@ export function shouldProfileTransaction(transaction: Transaction): boolean { return false; } - if (!transaction.isRecording()) { + if (!span.isRecording()) { if (DEBUG_BUILD) { logger.log('[Profiling] Discarding profile because transaction was not sampled.'); } diff --git a/packages/browser/src/stack-parsers.ts b/packages/browser/src/stack-parsers.ts index 5d09cde3cbbe..a5a303cd1375 100644 --- a/packages/browser/src/stack-parsers.ts +++ b/packages/browser/src/stack-parsers.ts @@ -153,7 +153,7 @@ const opera11: StackLineParserFn = line => { export const opera11StackLineParser: StackLineParser = [OPERA11_PRIORITY, opera11]; -export const defaultStackLineParsers = [chromeStackLineParser, geckoStackLineParser, winjsStackLineParser]; +export const defaultStackLineParsers = [chromeStackLineParser, geckoStackLineParser]; export const defaultStackParser = createStackParser(...defaultStackLineParsers); diff --git a/packages/browser/test/integration/debugging.md b/packages/browser/test/integration/debugging.md index f25d70d788ae..9f0cd250566a 100644 --- a/packages/browser/test/integration/debugging.md +++ b/packages/browser/test/integration/debugging.md @@ -1,26 +1,37 @@ ### Debugging Hints -These tests are hard to debug, because the testing system is somewhat complex, straightforward debugging doesn't work (see below), and the output of most `console.log` calls gets swallowed. Here, future debugger, are some tips to make it easier, to hopefully save you the hour(s) of trial and error it took to figure them out. +These tests are hard to debug, because the testing system is somewhat complex, straightforward debugging doesn't work +(see below), and the output of most `console.log` calls gets swallowed. Here, future debugger, are some tips to make it +easier, to hopefully save you the hour(s) of trial and error it took to figure them out. - `suites/shell.js`: + - Remove the loader options from the `variants` array. - - Delete all of the placeholders of the form `{{ suites/something.js }}` except for the one you're interested in. It's not enough to comment them out, because they'll still exist in the file and get replaced by the test runner. Don't forget the one at the bottom of the file. + - Delete all of the placeholders of the form `{{ suites/something.js }}` except for the one you're interested in. It's + not enough to comment them out, because they'll still exist in the file and get replaced by the test runner. Don't + forget the one at the bottom of the file. - `suites/helpers.js`: - - Add `sandbox.contentWindow.console.log = (...args) => console.log(...args);` just before the return in `createSandbox()`. This will make it so that `console.log` statements come through to the terminal. (Yes, Karma theoretically has settings for that, but they don't seem to work. See https://github.com/karma-runner/karma-mocha/issues/47.) + + - Add `sandbox.contentWindow.console.log = (...args) => console.log(...args);` just before the return in + `createSandbox()`. This will make it so that `console.log` statements come through to the terminal. (Yes, Karma + theoretically has settings for that, but they don't seem to work. See + https://github.com/karma-runner/karma-mocha/issues/47.) - `suites.yourTestFile.js`: + - Use `it.only` to only run the single test you're interested in. - Repo-level `rollup/bundleHelpers.js`: - - Comment out all bundle variants except whichever one `run.js` is turning into `artifacts/sdk.js`. -- Browser-package-level `rollup.bundle.config.mjs`: - - Build only one of `es5` and `es6`. + - Comment out all bundle variants except whichever one `run.js` is turning into `artifacts/sdk.js`. -- Run `build:bundle:watch` in a separate terminal tab, so that when you add `console.log`s to the SDK, they get picked up. +- Run `build:bundle:watch` in a separate terminal tab, so that when you add `console.log`s to the SDK, they get picked + up. -- Don't bother trying to copy one of our standard VSCode debug profiles, because it won't work, except to debug the testing system itself. (It will pause the node process running the tests, not the headless browser in which the tests themselves run.) +- Don't bother trying to copy one of our standard VSCode debug profiles, because it won't work, except to debug the + testing system itself. (It will pause the node process running the tests, not the headless browser in which the tests + themselves run.) - To make karma do verbose logging, run `export DEBUG=1`. To turn it off, run `unset DEBUG`. diff --git a/packages/browser/test/unit/index.bundle.feedback.test.ts b/packages/browser/test/unit/index.bundle.feedback.test.ts index 91475a34bb02..4ccb6a9dc458 100644 --- a/packages/browser/test/unit/index.bundle.feedback.test.ts +++ b/packages/browser/test/unit/index.bundle.feedback.test.ts @@ -1,6 +1,5 @@ -/* eslint-disable deprecation/deprecation */ -import { ReplayShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; -import { Feedback, feedbackIntegration } from '@sentry/browser'; +import { replayIntegrationShim } from '@sentry-internal/integration-shims'; +import { feedbackIntegration } from '@sentry/browser'; import * as TracingReplayBundle from '../../src/index.bundle.feedback'; @@ -10,11 +9,7 @@ describe('index.bundle.feedback', () => { expect((TracingReplayBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); }); - expect(TracingReplayBundle.Integrations.Replay).toBe(ReplayShim); - expect(TracingReplayBundle.Replay).toBe(ReplayShim); expect(TracingReplayBundle.replayIntegration).toBe(replayIntegrationShim); - - expect(TracingReplayBundle.Feedback).toBe(Feedback); expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegration); }); }); diff --git a/packages/browser/test/unit/index.bundle.replay.test.ts b/packages/browser/test/unit/index.bundle.replay.test.ts index a40daa2ea0d6..56356e262eea 100644 --- a/packages/browser/test/unit/index.bundle.replay.test.ts +++ b/packages/browser/test/unit/index.bundle.replay.test.ts @@ -1,6 +1,5 @@ -/* eslint-disable deprecation/deprecation */ -import { FeedbackShim, feedbackIntegrationShim } from '@sentry-internal/integration-shims'; -import { Replay, replayIntegration } from '@sentry/browser'; +import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; +import { replayIntegration } from '@sentry/browser'; import * as TracingReplayBundle from '../../src/index.bundle.replay'; @@ -10,11 +9,7 @@ describe('index.bundle.replay', () => { expect((TracingReplayBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); }); - expect(TracingReplayBundle.Integrations.Replay).toBe(Replay); - expect(TracingReplayBundle.Replay).toBe(Replay); expect(TracingReplayBundle.replayIntegration).toBe(replayIntegration); - - expect(TracingReplayBundle.Feedback).toBe(FeedbackShim); expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegrationShim); }); }); diff --git a/packages/browser/test/unit/index.bundle.test.ts b/packages/browser/test/unit/index.bundle.test.ts index a081c21a1e59..91cc4dea5229 100644 --- a/packages/browser/test/unit/index.bundle.test.ts +++ b/packages/browser/test/unit/index.bundle.test.ts @@ -1,10 +1,4 @@ -/* eslint-disable deprecation/deprecation */ -import { - FeedbackShim, - ReplayShim, - feedbackIntegrationShim, - replayIntegrationShim, -} from '@sentry-internal/integration-shims'; +import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; import * as TracingBundle from '../../src/index.bundle'; @@ -14,11 +8,7 @@ describe('index.bundle', () => { expect((TracingBundle.Integrations[key] as any).name).toStrictEqual(expect.any(String)); }); - expect(TracingBundle.Integrations.Replay).toBe(ReplayShim); - expect(TracingBundle.Replay).toBe(ReplayShim); expect(TracingBundle.replayIntegration).toBe(replayIntegrationShim); - - expect(TracingBundle.Feedback).toBe(FeedbackShim); expect(TracingBundle.feedbackIntegration).toBe(feedbackIntegrationShim); }); }); diff --git a/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts b/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts index 962934f064f8..49c23d9685bb 100644 --- a/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts +++ b/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts @@ -1,6 +1,5 @@ -/* eslint-disable deprecation/deprecation */ import { browserTracingIntegration } from '@sentry-internal/tracing'; -import { Feedback, Replay, feedbackIntegration, replayIntegration } from '@sentry/browser'; +import { feedbackIntegration, replayIntegration } from '@sentry/browser'; import * as TracingReplayFeedbackBundle from '../../src/index.bundle.tracing.replay.feedback'; @@ -10,13 +9,8 @@ describe('index.bundle.tracing.replay.feedback', () => { expect((TracingReplayFeedbackBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); }); - expect(TracingReplayFeedbackBundle.Integrations.Replay).toBe(Replay); - expect(TracingReplayFeedbackBundle.Replay).toBe(Replay); expect(TracingReplayFeedbackBundle.replayIntegration).toBe(replayIntegration); - expect(TracingReplayFeedbackBundle.browserTracingIntegration).toBe(browserTracingIntegration); - - expect(TracingReplayFeedbackBundle.Feedback).toBe(Feedback); expect(TracingReplayFeedbackBundle.feedbackIntegration).toBe(feedbackIntegration); }); }); diff --git a/packages/browser/test/unit/index.bundle.tracing.replay.test.ts b/packages/browser/test/unit/index.bundle.tracing.replay.test.ts index a90eac6cbe60..bdbb744f7873 100644 --- a/packages/browser/test/unit/index.bundle.tracing.replay.test.ts +++ b/packages/browser/test/unit/index.bundle.tracing.replay.test.ts @@ -1,7 +1,6 @@ -/* eslint-disable deprecation/deprecation */ -import { FeedbackShim, feedbackIntegrationShim } from '@sentry-internal/integration-shims'; +import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; import { browserTracingIntegration } from '@sentry-internal/tracing'; -import { Replay, replayIntegration } from '@sentry/browser'; +import { replayIntegration } from '@sentry/browser'; import * as TracingReplayBundle from '../../src/index.bundle.tracing.replay'; @@ -11,13 +10,10 @@ describe('index.bundle.tracing.replay', () => { expect((TracingReplayBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); }); - expect(TracingReplayBundle.Integrations.Replay).toBe(Replay); - expect(TracingReplayBundle.Replay).toBe(Replay); expect(TracingReplayBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayBundle.browserTracingIntegration).toBe(browserTracingIntegration); - expect(TracingReplayBundle.Feedback).toBe(FeedbackShim); expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegrationShim); }); }); diff --git a/packages/browser/test/unit/index.bundle.tracing.test.ts b/packages/browser/test/unit/index.bundle.tracing.test.ts index 0f8257ee4ad0..4c8c37008fc4 100644 --- a/packages/browser/test/unit/index.bundle.tracing.test.ts +++ b/packages/browser/test/unit/index.bundle.tracing.test.ts @@ -1,10 +1,4 @@ -/* eslint-disable deprecation/deprecation */ -import { - FeedbackShim, - ReplayShim, - feedbackIntegrationShim, - replayIntegrationShim, -} from '@sentry-internal/integration-shims'; +import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; import { browserTracingIntegration } from '@sentry-internal/tracing'; import * as TracingBundle from '../../src/index.bundle.tracing'; @@ -15,13 +9,8 @@ describe('index.bundle.tracing', () => { expect((TracingBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); }); - expect(TracingBundle.Integrations.Replay).toBe(ReplayShim); - expect(TracingBundle.Replay).toBe(ReplayShim); expect(TracingBundle.replayIntegration).toBe(replayIntegrationShim); - expect(TracingBundle.browserTracingIntegration).toBe(browserTracingIntegration); - - expect(TracingBundle.Feedback).toBe(FeedbackShim); expect(TracingBundle.feedbackIntegration).toBe(feedbackIntegrationShim); }); }); diff --git a/packages/browser/test/unit/tracekit/ie.test.ts b/packages/browser/test/unit/tracekit/ie.test.ts index 544542b0dcaf..53e96b96371f 100644 --- a/packages/browser/test/unit/tracekit/ie.test.ts +++ b/packages/browser/test/unit/tracekit/ie.test.ts @@ -1,5 +1,8 @@ +import { createStackParser } from '@sentry/utils'; import { exceptionFromError } from '../../../src/eventbuilder'; -import { defaultStackParser as parser } from '../../../src/stack-parsers'; +import { chromeStackLineParser, geckoStackLineParser, winjsStackLineParser } from '../../../src/stack-parsers'; + +const parser = createStackParser(chromeStackLineParser, geckoStackLineParser, winjsStackLineParser); describe('Tracekit - IE Tests', () => { it('should parse IE 10 error', () => { diff --git a/packages/browser/test/unit/tracekit/react.test.ts b/packages/browser/test/unit/tracekit/react.test.ts index d949a4dee0eb..55ffdc34c537 100644 --- a/packages/browser/test/unit/tracekit/react.test.ts +++ b/packages/browser/test/unit/tracekit/react.test.ts @@ -2,7 +2,7 @@ import { exceptionFromError } from '../../../src/eventbuilder'; import { defaultStackParser as parser } from '../../../src/stack-parsers'; describe('Tracekit - React Tests', () => { - it('should correctly parse Invariant Violation errors and use framesToPop to drop info message', () => { + it('should correctly parse Invariant Violation errors and use framesToPop to drop the invariant frame', () => { const REACT_INVARIANT_VIOLATION_EXCEPTION = { framesToPop: 1, message: @@ -38,13 +38,6 @@ describe('Tracekit - React Tests', () => { colno: 21841, in_app: true, }, - { - filename: 'http://localhost:5000/static/js/foo.chunk.js', - function: '?', - lineno: 1, - colno: 21738, - in_app: true, - }, ], }, }); @@ -97,7 +90,7 @@ describe('Tracekit - React Tests', () => { }); }); - it('should not drop additional frame for production errors if framesToPop is still there', () => { + it('should drop invariant frame for production errors if framesToPop is present', () => { const REACT_PRODUCTION_ERROR = { framesToPop: 1, message: @@ -133,13 +126,6 @@ describe('Tracekit - React Tests', () => { colno: 21841, in_app: true, }, - { - filename: 'http://localhost:5000/static/js/foo.chunk.js', - function: '?', - lineno: 1, - colno: 21738, - in_app: true, - }, ], }, }); diff --git a/packages/bun/.eslintrc.js b/packages/bun/.eslintrc.js index bec6469d0e28..9d915d4f4c3b 100644 --- a/packages/bun/.eslintrc.js +++ b/packages/bun/.eslintrc.js @@ -6,7 +6,6 @@ module.exports = { rules: { '@sentry-internal/sdk/no-optional-chaining': 'off', '@sentry-internal/sdk/no-nullish-coalescing': 'off', - '@sentry-internal/sdk/no-unsupported-es6-methods': 'off', '@sentry-internal/sdk/no-class-field-initializers': 'off', }, }; diff --git a/packages/bun/README.md b/packages/bun/README.md index d9c99350a613..43a80713e45b 100644 --- a/packages/bun/README.md +++ b/packages/bun/README.md @@ -15,7 +15,8 @@ - [Official SDK Docs](https://docs.sentry.io/quickstart/) - [TypeDoc](http://getsentry.github.io/sentry-javascript/) -The Sentry Bun SDK is in beta. Please help us improve the SDK by [reporting any issues or giving us feedback](https://github.com/getsentry/sentry-javascript/issues). +The Sentry Bun SDK is in beta. Please help us improve the SDK by +[reporting any issues or giving us feedback](https://github.com/getsentry/sentry-javascript/issues). ## Usage @@ -34,8 +35,8 @@ Sentry.init({ }); ``` -To set context information or send manual events, use the exported functions of `@sentry/bun`. Note that these -functions will not perform any action before you have called `init()`: +To set context information or send manual events, use the exported functions of `@sentry/bun`. Note that these functions +will not perform any action before you have called `init()`: ```javascript // Set user information, as well as tags and further extras @@ -60,8 +61,9 @@ Sentry.captureEvent({ }); ``` -It's not possible to capture unhandled exceptions, unhandled promise rejections now - Bun is working on adding support for it. -[Github Issue](https://github.com/oven-sh/bun/issues/5091) follow this issue. To report errors to Sentry, you have to manually try-catch and call `Sentry.captureException` in the catch block. +It's not possible to capture unhandled exceptions, unhandled promise rejections now - Bun is working on adding support +for it. [Github Issue](https://github.com/oven-sh/bun/issues/5091) follow this issue. To report errors to Sentry, you +have to manually try-catch and call `Sentry.captureException` in the catch block. ```ts import * as Sentry from '@sentry/bun'; @@ -72,4 +74,3 @@ try { Sentry.captureException(e); } ``` - diff --git a/packages/bun/package.json b/packages/bun/package.json index aa2aa15d1949..034c0cd0b948 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "files": [ "cjs", @@ -15,13 +15,26 @@ "types", "types-ts3.8" ], - "main": "build/esm/index.js", + "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/types/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/types/index.d.ts", + "default": "./build/cjs/index.js" + } + } + }, "typesVersions": { "<4.9": { - "build/npm/types/index.d.ts": [ - "build/npm/types-ts3.8/index.d.ts" + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" ] } }, @@ -30,7 +43,7 @@ }, "dependencies": { "@sentry/core": "8.0.0-alpha.2", - "@sentry/node-experimental": "8.0.0-alpha.2", + "@sentry/node": "8.0.0-alpha.2", "@sentry/types": "8.0.0-alpha.2", "@sentry/utils": "8.0.0-alpha.2" }, diff --git a/packages/bun/rollup.npm.config.mjs b/packages/bun/rollup.npm.config.mjs index 19f01d8cb3f8..84a06f2fb64a 100644 --- a/packages/bun/rollup.npm.config.mjs +++ b/packages/bun/rollup.npm.config.mjs @@ -1,6 +1,3 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -const config = makeNPMConfigVariants(makeBaseNPMConfig()); - -// remove cjs from config array config[0].output.format == cjs -export default [config[1]]; +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/bun/src/client.ts b/packages/bun/src/client.ts index 7b3a350f2821..40e430dc2545 100644 --- a/packages/bun/src/client.ts +++ b/packages/bun/src/client.ts @@ -1,7 +1,6 @@ import * as os from 'os'; import type { ServerRuntimeClientOptions } from '@sentry/core'; -import { applySdkMetadata } from '@sentry/core'; -import { ServerRuntimeClient } from '@sentry/core'; +import { ServerRuntimeClient, applySdkMetadata } from '@sentry/core'; import type { BunClientOptions } from './types'; diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 80ad94b8e460..da25f6cb08bd 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -18,23 +18,19 @@ export type { } from '@sentry/types'; export type { AddRequestDataToEventOptions } from '@sentry/utils'; -export type { TransactionNamingScheme } from '@sentry/node-experimental'; -export type { BunOptions } from './types'; - export { - // eslint-disable-next-line deprecation/deprecation - addGlobalEventProcessor, addEventProcessor, addBreadcrumb, addIntegration, captureException, captureEvent, captureMessage, - close, + captureCheckIn, + startSession, + captureSession, + endSession, + withMonitor, createTransport, - flush, - // eslint-disable-next-line deprecation/deprecation - getActiveTransaction, // eslint-disable-next-line deprecation/deprecation getCurrentHub, getClient, @@ -43,12 +39,8 @@ export { getGlobalScope, getIsolationScope, Hub, - // eslint-disable-next-line deprecation/deprecation - makeMain, setCurrentClient, Scope, - // eslint-disable-next-line deprecation/deprecation - startTransaction, SDK_VERSION, setContext, setExtra, @@ -60,71 +52,80 @@ export { setHttpStatus, withScope, withIsolationScope, - captureCheckIn, - withMonitor, + makeNodeTransport, + NodeClient, + defaultStackParser, + flush, + close, + getSentryRelease, + addRequestDataToEvent, + DEFAULT_USER_INCLUDES, + extractRequestData, + createGetModuleFromFilename, + anrIntegration, + consoleIntegration, + httpIntegration, + nativeNodeFetchIntegration, + onUncaughtExceptionIntegration, + onUnhandledRejectionIntegration, + modulesIntegration, + contextLinesIntegration, + nodeContextIntegration, + localVariablesIntegration, + requestDataIntegration, + functionToStringIntegration, + inboundFiltersIntegration, + linkedErrorsIntegration, setMeasurement, getActiveSpan, startSpan, startInactiveSpan, startSpanManual, withActiveSpan, + getRootSpan, + getSpanDescendants, continueTrace, - metricsDefault as metrics, - functionToStringIntegration, - inboundFiltersIntegration, - linkedErrorsIntegration, - requestDataIntegration, + getAutoPerformanceIntegrations, + cron, + metrics, + parameterize, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + expressIntegration, + expressErrorHandler, + setupExpressErrorHandler, + fastifyIntegration, + setupFastifyErrorHandler, + graphqlIntegration, + mongoIntegration, + mongooseIntegration, + mysqlIntegration, + mysql2Integration, + nestIntegration, + postgresIntegration, + prismaIntegration, + hapiIntegration, + setupHapiErrorHandler, + spotlightIntegration, +} from '@sentry/node'; + +export { captureConsoleIntegration, debugIntegration, dedupeIntegration, extraErrorDataIntegration, rewriteFramesIntegration, sessionTimingIntegration, - parameterize, - startSession, - captureSession, - endSession, } from '@sentry/core'; -export { - DEFAULT_USER_INCLUDES, - autoDiscoverNodePerformanceMonitoringIntegrations, - cron, - createGetModuleFromFilename, - defaultStackParser, - extractRequestData, - getSentryRelease, - addRequestDataToEvent, - anrIntegration, - consoleIntegration, - contextLinesIntegration, - hapiIntegration, - httpIntegration, - localVariablesIntegration, - modulesIntegration, - nativeNodeFetchintegration, - nodeContextIntegration, - onUncaughtExceptionIntegration, - onUnhandledRejectionIntegration, - spotlightIntegration, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/node-experimental'; + +export type { BunOptions } from './types'; export { BunClient } from './client'; export { getDefaultIntegrations, init, } from './sdk'; - -import { Integrations as NodeIntegrations } from '@sentry/node-experimental'; -import { BunServer } from './integrations/bunserver'; export { bunServerIntegration } from './integrations/bunserver'; - -const INTEGRATIONS = { - ...NodeIntegrations, - BunServer, -}; - -export { INTEGRATIONS as Integrations }; +export { makeFetchTransport } from './transports'; diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index f5b6a52d3dd0..a530fc0517c2 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -4,7 +4,6 @@ import { Transaction, captureException, continueTrace, - convertIntegrationFnToClass, defineIntegration, getCurrentScope, setHttpStatus, @@ -25,15 +24,20 @@ const _bunServerIntegration = (() => { }; }) satisfies IntegrationFn; -export const bunServerIntegration = defineIntegration(_bunServerIntegration); - /** * Instruments `Bun.serve` to automatically create transactions and capture errors. * - * @deprecated Use `bunServerIntegration()` instead. + * Enabled by default in the Bun SDK. + * + * ```js + * Sentry.init({ + * integrations: [ + * Sentry.bunServerIntegration(), + * ], + * }) + * ``` */ -// eslint-disable-next-line deprecation/deprecation -export const BunServer = convertIntegrationFnToClass(INTEGRATION_NAME, bunServerIntegration); +export const bunServerIntegration = defineIntegration(_bunServerIntegration); /** * Instruments Bun.serve by patching it's options. @@ -82,13 +86,12 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] return continueTrace( { sentryTrace: request.headers.get('sentry-trace') || '', baggage: request.headers.get('baggage') }, - ctx => { + () => { return startSpan( { attributes, op: 'http.server', name: `${request.method} ${parsedUrl.path || '/'}`, - ...ctx, }, async span => { try { @@ -96,9 +99,7 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] typeof serveOptions.fetch >); if (response && response.status) { - if (span) { - setHttpStatus(span, response.status); - } + setHttpStatus(span, response.status); if (span instanceof Transaction) { const scope = getCurrentScope(); scope.setContext('response', { diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts index b7eddfed9c73..51099eb814b2 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-lines */ import { functionToStringIntegration, inboundFiltersIntegration, @@ -11,9 +10,9 @@ import { httpIntegration, init as initNode, modulesIntegration, - nativeNodeFetchintegration, + nativeNodeFetchIntegration, nodeContextIntegration, -} from '@sentry/node-experimental'; +} from '@sentry/node'; import type { Integration, Options } from '@sentry/types'; import { BunClient } from './client'; @@ -33,7 +32,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] { // Native Wrappers consoleIntegration(), httpIntegration(), - nativeNodeFetchintegration(), + nativeNodeFetchIntegration(), // Global Handlers # TODO (waiting for https://github.com/oven-sh/bun/issues/5091) // new NodeIntegrations.OnUncaughtException(), // new NodeIntegrations.OnUnhandledRejection(), diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index 14080fa02315..b1dc17381ccb 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -23,11 +23,11 @@ describe('Bun Serve Integration', () => { }); test('generates a transaction around a request', async () => { - client.on('finishTransaction', transaction => { - expect(spanToJSON(transaction).status).toBe('ok'); - expect(spanToJSON(transaction).data?.['http.response.status_code']).toEqual(200); - expect(spanToJSON(transaction).op).toEqual('http.server'); - expect(spanToJSON(transaction).description).toEqual('GET /'); + client.on('spanEnd', span => { + expect(spanToJSON(span).status).toBe('ok'); + expect(spanToJSON(span).data?.['http.response.status_code']).toEqual(200); + expect(spanToJSON(span).op).toEqual('http.server'); + expect(spanToJSON(span).description).toEqual('GET /'); }); const server = Bun.serve({ @@ -43,11 +43,11 @@ describe('Bun Serve Integration', () => { }); test('generates a post transaction', async () => { - client.on('finishTransaction', transaction => { - expect(spanToJSON(transaction).status).toBe('ok'); - expect(spanToJSON(transaction).data?.['http.response.status_code']).toEqual(200); - expect(spanToJSON(transaction).op).toEqual('http.server'); - expect(spanToJSON(transaction).description).toEqual('POST /'); + client.on('spanEnd', span => { + expect(spanToJSON(span).status).toBe('ok'); + expect(spanToJSON(span).data?.['http.response.status_code']).toEqual(200); + expect(spanToJSON(span).op).toEqual('http.server'); + expect(spanToJSON(span).description).toEqual('POST /'); }); const server = Bun.serve({ @@ -72,16 +72,13 @@ describe('Bun Serve Integration', () => { const SENTRY_TRACE_HEADER = `${TRACE_ID}-${PARENT_SPAN_ID}-${PARENT_SAMPLED}`; const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-environment=production'; - client.on('finishTransaction', transaction => { - expect(transaction.spanContext().traceId).toBe(TRACE_ID); - expect(transaction.parentSpanId).toBe(PARENT_SPAN_ID); - expect(spanIsSampled(transaction)).toBe(true); - // span.endTimestamp is already set in `finishTransaction` hook - expect(transaction.isRecording()).toBe(false); + client.on('spanEnd', span => { + expect(span.spanContext().traceId).toBe(TRACE_ID); + expect(spanToJSON(span).parent_span_id).toBe(PARENT_SPAN_ID); + expect(spanIsSampled(span)).toBe(true); + expect(span.isRecording()).toBe(false); - // eslint-disable-next-line deprecation/deprecation - expect(transaction.metadata?.dynamicSamplingContext).toStrictEqual({ version: '1.0', environment: 'production' }); - expect(getDynamicSamplingContextFromSpan(transaction)).toStrictEqual({ + expect(getDynamicSamplingContextFromSpan(span)).toStrictEqual({ version: '1.0', environment: 'production', }); @@ -102,7 +99,7 @@ describe('Bun Serve Integration', () => { }); test('does not create transactions for OPTIONS or HEAD requests', async () => { - client.on('finishTransaction', () => { + client.on('spanEnd', () => { // This will never run, but we want to make sure it doesn't run. expect(false).toEqual(true); }); diff --git a/packages/bun/tsconfig.json b/packages/bun/tsconfig.json index 70c4bde02040..dcbef254b942 100644 --- a/packages/bun/tsconfig.json +++ b/packages/bun/tsconfig.json @@ -4,21 +4,7 @@ "include": ["src/**/*"], "compilerOptions": { - "types": ["bun-types"], - "lib": ["esnext"], - "module": "esnext", - "target": "esnext", - - // if TS 4.x or earlier - "moduleResolution": "nodenext", - - "jsx": "react-jsx", // support JSX - "allowJs": true, // allow importing `.js` from `.ts` - "esModuleInterop": true, // allow default imports for CommonJS modules - - // best practices - "strict": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true + // package-specific options + "types": ["bun-types"] } } diff --git a/packages/core/package.json b/packages/core/package.json index 6c58cbf762ee..24f7e29a036a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "files": [ "cjs", @@ -18,6 +18,19 @@ "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/types/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/types/index.d.ts", + "default": "./build/cjs/index.js" + } + } + }, "typesVersions": { "<4.9": { "build/types/index.d.ts": [ diff --git a/packages/core/rollup.npm.config.mjs b/packages/core/rollup.npm.config.mjs index 84a06f2fb64a..fd61fbf7c62c 100644 --- a/packages/core/rollup.npm.config.mjs +++ b/packages/core/rollup.npm.config.mjs @@ -1,3 +1,14 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -export default makeNPMConfigVariants(makeBaseNPMConfig()); +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + packageSpecificConfig: { + output: { + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + // set preserveModules to false because we want to bundle everything into one file. + preserveModules: false, + }, + }, + }), +); diff --git a/packages/core/src/asyncContext.ts b/packages/core/src/asyncContext.ts index fa47ce8aa020..854b03ea9600 100644 --- a/packages/core/src/asyncContext.ts +++ b/packages/core/src/asyncContext.ts @@ -1,6 +1,8 @@ import type { Hub, Integration } from '@sentry/types'; import type { Scope } from '@sentry/types'; import { GLOBAL_OBJ } from '@sentry/utils'; +import type { startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './tracing/trace'; +import type { getActiveSpan } from './utils/spanUtils'; /** * @private Private API with no semver guarantees! @@ -42,6 +44,24 @@ export interface AsyncContextStrategy { * Get the currently active isolation scope. */ getIsolationScope: () => Scope; + + // OPTIONAL: Custom tracing methods + // These are used so that we can provide OTEL-based implementations + + /** Start an active span. */ + startSpan?: typeof startSpan; + + /** Start an inactive span. */ + startInactiveSpan?: typeof startInactiveSpan; + + /** Start an active manual span. */ + startSpanManual?: typeof startSpanManual; + + /** Get the currently active span. */ + getActiveSpan?: typeof getActiveSpan; + + /** Make a span the active span in the context of the callback. */ + withActiveSpan?: typeof withActiveSpan; } /** diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 8d36844a96bf..178a37aff5c0 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -22,8 +22,8 @@ import type { Session, SessionAggregates, SeverityLevel, + Span, StartSpanOptions, - Transaction, TransactionEvent, Transport, TransportMakeRequestResponse, @@ -417,10 +417,13 @@ export abstract class BaseClient implements Client { /* eslint-disable @typescript-eslint/unified-signatures */ /** @inheritdoc */ - public on(hook: 'startTransaction', callback: (transaction: Transaction) => void): void; + public on(hook: 'spanStart', callback: (span: Span) => void): void; /** @inheritdoc */ - public on(hook: 'finishTransaction', callback: (transaction: Transaction) => void): void; + public on(hook: 'spanEnd', callback: (span: Span) => void): void; + + /** @inheritdoc */ + public on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): void; /** @inheritdoc */ public on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): void; @@ -447,7 +450,13 @@ export abstract class BaseClient implements Client { ): void; /** @inheritdoc */ - public on(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void; + public on( + hook: 'startPageLoadSpan', + callback: ( + options: StartSpanOptions, + traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined }, + ) => void, + ): void; /** @inheritdoc */ public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; @@ -467,10 +476,13 @@ export abstract class BaseClient implements Client { } /** @inheritdoc */ - public emit(hook: 'startTransaction', transaction: Transaction): void; + public emit(hook: 'spanStart', span: Span): void; /** @inheritdoc */ - public emit(hook: 'finishTransaction', transaction: Transaction): void; + public emit(hook: 'spanEnd', span: Span): void; + + /** @inheritdoc */ + public emit(hook: 'idleSpanEnableAutoFinish', span: Span): void; /** @inheritdoc */ public emit(hook: 'beforeEnvelope', envelope: Envelope): void; @@ -494,7 +506,11 @@ export abstract class BaseClient implements Client { public emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay: boolean }): void; /** @inheritdoc */ - public emit(hook: 'startPageLoadSpan', options: StartSpanOptions): void; + public emit( + hook: 'startPageLoadSpan', + options: StartSpanOptions, + traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined }, + ): void; /** @inheritdoc */ public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void; @@ -515,7 +531,7 @@ export abstract class BaseClient implements Client { /** * @inheritdoc */ - public sendEnvelope(envelope: Envelope): PromiseLike | void { + public sendEnvelope(envelope: Envelope): PromiseLike { this.emit('beforeEnvelope', envelope); if (this._isEnabled() && this._transport) { @@ -526,6 +542,8 @@ export abstract class BaseClient implements Client { } DEBUG_BUILD && logger.error('Transport disabled'); + + return resolvedSyncPromise({}); } /* eslint-enable @typescript-eslint/unified-signatures */ diff --git a/packages/core/src/currentScopes.ts b/packages/core/src/currentScopes.ts index 07e26d9a96b6..1e68c9583372 100644 --- a/packages/core/src/currentScopes.ts +++ b/packages/core/src/currentScopes.ts @@ -1,15 +1,10 @@ import type { Scope } from '@sentry/types'; import type { Client } from '@sentry/types'; +import { getGlobalSingleton } from '@sentry/utils'; import { getMainCarrier } from './asyncContext'; import { getAsyncContextStrategy } from './hub'; import { Scope as ScopeClass } from './scope'; -/** - * The global scope is kept in this module. - * When accessing it, we'll make sure to set one if none is currently present. - */ -let globalScope: Scope | undefined; - /** * Get the currently active scope. */ @@ -34,20 +29,7 @@ export function getIsolationScope(): Scope { * This scope is applied to _all_ events. */ export function getGlobalScope(): Scope { - if (!globalScope) { - globalScope = new ScopeClass(); - } - - return globalScope; -} - -/** - * This is mainly needed for tests. - * DO NOT USE this, as this is an internal API and subject to change. - * @hidden - */ -export function setGlobalScope(scope: Scope | undefined): void { - globalScope = scope; + return getGlobalSingleton('globalScope', () => new ScopeClass()); } /** diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 9ec29c9d2a7e..3189cdc5278a 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -1,4 +1,6 @@ import type { + Attachment, + AttachmentItem, DsnComponents, Event, EventEnvelope, @@ -11,6 +13,7 @@ import type { SessionItem, } from '@sentry/types'; import { + createAttachmentEnvelopeItem, createEnvelope, createEventEnvelopeHeaders, dsnToString, @@ -86,3 +89,31 @@ export function createEventEnvelope( const eventItem: EventItem = [{ type: eventType }, event]; return createEnvelope(envelopeHeaders, [eventItem]); } + +/** + * Create an Envelope from an event. + */ +export function createAttachmentEnvelope( + event: Event, + attachments: Attachment[], + dsn?: DsnComponents, + metadata?: SdkMetadata, + tunnel?: string, +): EventEnvelope { + const sdkInfo = getSdkMetadataForEnvelopeHeader(metadata); + enhanceEventWithSdkInfo(event, metadata && metadata.sdk); + + const envelopeHeaders = createEventEnvelopeHeaders(event, sdkInfo, tunnel, dsn); + + // Prevent this data (which, if it exists, was used in earlier steps in the processing pipeline) from being sent to + // sentry. (Note: Our use of this property comes and goes with whatever we might be debugging, whatever hacks we may + // have temporarily added, etc. Even if we don't happen to be using it at some point in the future, let's not get rid + // of this `delete`, lest we miss putting it back in the next time the property is in use.) + delete event.sdkProcessingMetadata; + + const attachmentItems: AttachmentItem[] = []; + for (const attachment of attachments || []) { + attachmentItems.push(createAttachmentEnvelopeItem(attachment)); + } + return createEnvelope(envelopeHeaders, attachmentItems); +} diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index eb9a95652792..2ccb6ef530e8 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -1,7 +1,6 @@ import type { CaptureContext, CheckIn, - CustomSamplingContext, Event, EventHint, EventProcessor, @@ -10,21 +9,17 @@ import type { FinishedCheckIn, MonitorConfig, Primitive, - Scope as ScopeInterface, Session, SessionContext, SeverityLevel, - Span, - TransactionContext, User, } from '@sentry/types'; import { GLOBAL_OBJ, isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import { DEFAULT_ENVIRONMENT } from './constants'; -import { getClient, getCurrentScope, getIsolationScope, withScope } from './currentScopes'; +import { getClient, getCurrentScope, getIsolationScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { Hub } from './hub'; -import { getCurrentHub } from './hub'; import { closeSession, makeSession, updateSession } from './session'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; @@ -126,53 +121,6 @@ export function setUser(user: User | null): ReturnType { getIsolationScope().setUser(user); } -/** - * Forks the current scope and sets the provided span as active span in the context of the provided callback. Can be - * passed `null` to start an entirely new span tree. - * - * @param span Spans started in the context of the provided callback will be children of this span. If `null` is passed, - * spans started within the callback will not be attached to a parent span. - * @param callback Execution context in which the provided span will be active. Is passed the newly forked scope. - * @returns the value returned from the provided callback function. - */ -export function withActiveSpan(span: Span | null, callback: (scope: ScopeInterface) => T): T { - return withScope(scope => { - // eslint-disable-next-line deprecation/deprecation - scope.setSpan(span || undefined); - return callback(scope); - }); -} - -/** - * Starts a new `Transaction` and returns it. This is the entry point to manual tracing instrumentation. - * - * A tree structure can be built by adding child spans to the transaction, and child spans to other spans. To start a - * new child span within the transaction or any span, call the respective `.startChild()` method. - * - * Every child span must be finished before the transaction is finished, otherwise the unfinished spans are discarded. - * - * The transaction must be finished with a call to its `.end()` method, at which point the transaction with all its - * finished child spans will be sent to Sentry. - * - * NOTE: This function should only be used for *manual* instrumentation. Auto-instrumentation should call - * `startTransaction` directly on the hub. - * - * @param context Properties of the new `Transaction`. - * @param customSamplingContext Information given to the transaction sampling function (along with context-dependent - * default values). See {@link Options.tracesSampler}. - * - * @returns The transaction which was just started - * - * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. - */ -export function startTransaction( - context: TransactionContext, - customSamplingContext?: CustomSamplingContext, -): ReturnType { - // eslint-disable-next-line deprecation/deprecation - return getCurrentHub().startTransaction({ ...context }, customSamplingContext); -} - /** * Create a cron monitor check in and send it to Sentry. * diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/core/src/fetch.ts similarity index 92% rename from packages/tracing-internal/src/common/fetch.ts rename to packages/core/src/fetch.ts index aa50154abc5a..5255e7fa206d 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/core/src/fetch.ts @@ -1,16 +1,3 @@ -import { - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - getClient, - getCurrentScope, - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - getIsolationScope, - hasTracingEnabled, - setHttpStatus, - spanToTraceHeader, - startInactiveSpan, -} from '@sentry/core'; import type { Client, HandlerDataFetch, Scope, Span, SpanOrigin } from '@sentry/types'; import { BAGGAGE_HEADER_NAME, @@ -18,6 +5,18 @@ import { generateSentryTraceHeader, isInstanceOf, } from '@sentry/utils'; +import { getClient, getCurrentScope, getIsolationScope } from './currentScopes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes'; +import { + SPAN_STATUS_ERROR, + getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, + setHttpStatus, + startInactiveSpan, +} from './tracing'; +import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; +import { hasTracingEnabled } from './utils/hasTracingEnabled'; +import { spanToTraceHeader } from './utils/spanUtils'; type PolymorphicRequestHeaders = | Record @@ -94,12 +93,10 @@ export function instrumentFetchRequest( }, op: 'http.client', }) - : undefined; + : new SentryNonRecordingSpan(); - if (span) { - handlerData.fetchData.__span = span.spanContext().spanId; - spans[span.spanContext().spanId] = span; - } + handlerData.fetchData.__span = span.spanContext().spanId; + spans[span.spanContext().spanId] = span; if (shouldAttachHeaders(handlerData.fetchData.url) && client) { const request: string | Request = handlerData.args[0]; @@ -110,7 +107,6 @@ export function instrumentFetchRequest( // eslint-disable-next-line @typescript-eslint/no-explicit-any const options: { [key: string]: any } = handlerData.args[1]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access options.headers = addTracingHeadersToFetchRequest(request, client, scope, options, span); } diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index abfb22b612b3..a60b3af506fc 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -3,7 +3,6 @@ import type { Breadcrumb, BreadcrumbHint, Client, - CustomSamplingContext, Event, EventHint, Extra, @@ -16,10 +15,9 @@ import type { Session, SessionContext, SeverityLevel, - Transaction, - TransactionContext, User, } from '@sentry/types'; +import { getGlobalSingleton } from '@sentry/utils'; import { GLOBAL_OBJ, consoleSandbox, dateTimestampInSeconds, isThenable, logger, uuid4 } from '@sentry/utils'; import type { AsyncContextStrategy, Carrier } from './asyncContext'; @@ -438,46 +436,6 @@ export class Hub implements HubInterface { } } - /** - * Starts a new `Transaction` and returns it. This is the entry point to manual tracing instrumentation. - * - * A tree structure can be built by adding child spans to the transaction, and child spans to other spans. To start a - * new child span within the transaction or any span, call the respective `.startChild()` method. - * - * Every child span must be finished before the transaction is finished, otherwise the unfinished spans are discarded. - * - * The transaction must be finished with a call to its `.end()` method, at which point the transaction with all its - * finished child spans will be sent to Sentry. - * - * @param context Properties of the new `Transaction`. - * @param customSamplingContext Information given to the transaction sampling function (along with context-dependent - * default values). See {@link Options.tracesSampler}. - * - * @returns The transaction which was just started - * - * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. - */ - public startTransaction(context: TransactionContext, customSamplingContext?: CustomSamplingContext): Transaction { - 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( - "Tracing extension 'startTransaction' is missing. You should 'init' the SDK before calling 'startTransaction'", - ); - } else { - logger.warn(`Tracing extension 'startTransaction' has not been added. Call 'addTracingExtensions' before calling 'init': -Sentry.addTracingExtensions(); -Sentry.init({...}); -`); - } - } - - return result; - } - /** * @inheritDoc * @@ -617,25 +575,14 @@ export function getCurrentHub(): HubInterface { return acs.getCurrentHub() || getGlobalHub(); } -let defaultCurrentScope: Scope | undefined; -let defaultIsolationScope: Scope | undefined; - /** Get the default current scope. */ export function getDefaultCurrentScope(): Scope { - if (!defaultCurrentScope) { - defaultCurrentScope = new Scope(); - } - - return defaultCurrentScope; + return getGlobalSingleton('defaultCurrentScope', () => new Scope()); } /** Get the default isolation scope. */ export function getDefaultIsolationScope(): Scope { - if (!defaultIsolationScope) { - defaultIsolationScope = new Scope(); - } - - return defaultIsolationScope; + return getGlobalSingleton('defaultIsolationScope', () => new Scope()); } /** diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b8c451b432cd..fbf233ad8dd8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,7 +8,7 @@ export type { IntegrationIndex } from './integration'; export * from './tracing'; export * from './semanticAttributes'; -export { createEventEnvelope, createSessionEnvelope } from './envelope'; +export { createEventEnvelope, createSessionEnvelope, createAttachmentEnvelope } from './envelope'; export { captureCheckIn, withMonitor, @@ -17,8 +17,6 @@ export { captureMessage, close, flush, - // eslint-disable-next-line deprecation/deprecation - startTransaction, setContext, setExtra, setExtras, @@ -29,7 +27,6 @@ export { startSession, endSession, captureSession, - withActiveSpan, addEventProcessor, } from './exports'; export { @@ -46,7 +43,6 @@ export { getCurrentScope, getIsolationScope, getGlobalScope, - setGlobalScope, withScope, withIsolationScope, getClient, @@ -90,8 +86,12 @@ export { spanToJSON, spanIsSampled, spanToTraceContext, + getSpanDescendants, + getStatusMessage, + getRootSpan, + getActiveSpan, + addChildSpanToSpan, } from './utils/spanUtils'; -export { getRootSpan } from './utils/getRootSpan'; export { applySdkMetadata } from './utils/sdkMetadata'; export { DEFAULT_ENVIRONMENT } from './constants'; /* eslint-disable deprecation/deprecation */ @@ -113,3 +113,5 @@ export { metrics } from './metrics/exports'; export type { MetricData } from './metrics/exports'; export { metricsDefault } from './metrics/exports-default'; export { BrowserMetricsAggregator } from './metrics/browser-aggregator'; +export { getMetricSummaryJsonForSpan } from './metrics/metric-summary'; +export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch'; diff --git a/packages/core/src/metrics/aggregator.ts b/packages/core/src/metrics/aggregator.ts index 5f0337e804f3..169e40b42905 100644 --- a/packages/core/src/metrics/aggregator.ts +++ b/packages/core/src/metrics/aggregator.ts @@ -1,15 +1,9 @@ -import type { - ClientOptions, - MeasurementUnit, - MetricsAggregator as MetricsAggregatorBase, - Primitive, -} from '@sentry/types'; +import type { Client, MeasurementUnit, MetricsAggregator as MetricsAggregatorBase, Primitive } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; -import type { BaseClient } from '../baseclient'; +import { updateMetricSummaryOnActiveSpan } from '../utils/spanUtils'; import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants'; import { captureAggregateMetrics } from './envelope'; import { METRIC_MAP } from './instance'; -import { updateMetricSummaryOnActiveSpan } from './metric-summary'; import type { MetricBucket, MetricType } from './types'; import { getBucketKey, sanitizeTags } from './utils'; @@ -40,7 +34,7 @@ export class MetricsAggregator implements MetricsAggregatorBase { // Force flush is used on either shutdown, flush() or when we exceed the max weight. private _forceFlush: boolean; - public constructor(private readonly _client: BaseClient) { + public constructor(private readonly _client: Client) { this._buckets = new Map(); this._bucketsTotalWeight = 0; this._interval = setInterval(() => this._flush(), DEFAULT_FLUSH_INTERVAL); diff --git a/packages/core/src/metrics/browser-aggregator.ts b/packages/core/src/metrics/browser-aggregator.ts index d19aa441aef3..7d599f5aeba8 100644 --- a/packages/core/src/metrics/browser-aggregator.ts +++ b/packages/core/src/metrics/browser-aggregator.ts @@ -1,10 +1,9 @@ -import type { ClientOptions, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types'; +import type { Client, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; -import type { BaseClient } from '../baseclient'; +import { updateMetricSummaryOnActiveSpan } from '../utils/spanUtils'; import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants'; import { captureAggregateMetrics } from './envelope'; import { METRIC_MAP } from './instance'; -import { updateMetricSummaryOnActiveSpan } from './metric-summary'; import type { MetricBucket, MetricType } from './types'; import { getBucketKey, sanitizeTags } from './utils'; @@ -21,7 +20,7 @@ export class BrowserMetricsAggregator implements MetricsAggregator { private _buckets: MetricBucket; private readonly _interval: ReturnType; - public constructor(private readonly _client: BaseClient) { + public constructor(private readonly _client: Client) { this._buckets = new Map(); this._interval = setInterval(() => this.flush(), DEFAULT_BROWSER_FLUSH_INTERVAL); } diff --git a/packages/core/src/metrics/envelope.ts b/packages/core/src/metrics/envelope.ts index 47ccc2740834..f3bd67b27ec6 100644 --- a/packages/core/src/metrics/envelope.ts +++ b/packages/core/src/metrics/envelope.ts @@ -1,22 +1,11 @@ -import type { - ClientOptions, - DsnComponents, - MetricBucketItem, - SdkMetadata, - StatsdEnvelope, - StatsdItem, -} from '@sentry/types'; +import type { Client, DsnComponents, MetricBucketItem, SdkMetadata, StatsdEnvelope, StatsdItem } from '@sentry/types'; import { createEnvelope, dsnToString, logger } from '@sentry/utils'; -import type { BaseClient } from '../baseclient'; import { serializeMetricBuckets } from './utils'; /** * Captures aggregated metrics to the supplied client. */ -export function captureAggregateMetrics( - client: BaseClient, - metricBucketItems: Array, -): void { +export function captureAggregateMetrics(client: Client, metricBucketItems: Array): void { logger.log(`Flushing aggregated metrics, number of metrics: ${metricBucketItems.length}`); const dsn = client.getDsn(); const metadata = client.getSdkMetadata(); diff --git a/packages/core/src/metrics/exports-default.ts b/packages/core/src/metrics/exports-default.ts index f331bd3de3f1..280d1d619bea 100644 --- a/packages/core/src/metrics/exports-default.ts +++ b/packages/core/src/metrics/exports-default.ts @@ -1,3 +1,4 @@ +import type { Client, MetricsAggregator as MetricsAggregatorInterface } from '@sentry/types'; import { MetricsAggregator } from './aggregator'; import type { MetricData } from './exports'; import { metrics as metricsCore } from './exports'; @@ -38,9 +39,20 @@ function gauge(name: string, value: number, data?: MetricData): void { metricsCore.gauge(MetricsAggregator, name, value, data); } +/** + * Returns the metrics aggregator for a given client. + */ +function getMetricsAggregatorForClient(client: Client): MetricsAggregatorInterface { + return metricsCore.getMetricsAggregatorForClient(client, MetricsAggregator); +} + export const metricsDefault = { increment, distribution, set, gauge, + /** + * @ignore This is for internal use only. + */ + getMetricsAggregatorForClient, }; diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index 4587c26a8510..2ed0d9cb9d51 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -1,11 +1,10 @@ import type { - ClientOptions, + Client, MeasurementUnit, MetricsAggregator as MetricsAggregatorInterface, Primitive, } from '@sentry/types'; -import { logger } from '@sentry/utils'; -import type { BaseClient } from '../baseclient'; +import { getGlobalSingleton, logger } from '@sentry/utils'; import { getCurrentScope } from '../currentScopes'; import { getClient } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; @@ -17,18 +16,39 @@ export interface MetricData { unit?: MeasurementUnit; tags?: Record; timestamp?: number; + client?: Client; } type MetricsAggregatorConstructor = { - new (client: BaseClient): MetricsAggregatorInterface; + new (client: Client): MetricsAggregatorInterface; }; /** - * Global metrics aggregator instance. - * - * This is initialized on the first call to any `Sentry.metric.*` method. + * Gets the metrics aggregator for a given client. + * @param client The client for which to get the metrics aggregator. + * @param Aggregator Optional metrics aggregator class to use to create an aggregator if one does not exist. */ -let globalMetricsAggregator: MetricsAggregatorInterface | undefined; +function getMetricsAggregatorForClient( + client: Client, + Aggregator: MetricsAggregatorConstructor, +): MetricsAggregatorInterface { + const globalMetricsAggregators = getGlobalSingleton>( + 'globalMetricsAggregators', + () => new WeakMap(), + ); + + const aggregator = globalMetricsAggregators.get(client); + if (aggregator) { + return aggregator; + } + + const newAggregator = new Aggregator(client); + client.on('flush', () => newAggregator.flush()); + client.on('close', () => newAggregator.close()); + globalMetricsAggregators.set(client, newAggregator); + + return newAggregator; +} function addToMetricsAggregator( Aggregator: MetricsAggregatorConstructor, @@ -37,38 +57,32 @@ function addToMetricsAggregator( value: number | string, data: MetricData | undefined = {}, ): void { - const client = getClient>(); + const client = data.client || getClient(); + if (!client) { return; } - if (!globalMetricsAggregator) { - const aggregator = (globalMetricsAggregator = new Aggregator(client)); - - client.on('flush', () => aggregator.flush()); - client.on('close', () => aggregator.close()); + const scope = getCurrentScope(); + 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) { + metricTags.release = release; } - - if (client) { - const scope = getCurrentScope(); - 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) { - metricTags.release = release; - } - if (environment) { - metricTags.environment = environment; - } - if (transaction) { - metricTags.transaction = spanToJSON(transaction).description || ''; - } - - DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`); - globalMetricsAggregator.add(metricType, name, value, unit, { ...metricTags, ...tags }, timestamp); + if (environment) { + metricTags.environment = environment; } + if (transaction) { + metricTags.transaction = spanToJSON(transaction).description || ''; + } + + DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`); + + const aggregator = getMetricsAggregatorForClient(client, Aggregator); + aggregator.add(metricType, name, value, unit, { ...metricTags, ...tags }, timestamp); } /** @@ -112,4 +126,8 @@ export const metrics = { distribution, set, gauge, + /** + * @ignore This is for internal use only. + */ + getMetricsAggregatorForClient, }; diff --git a/packages/core/src/metrics/metric-summary.ts b/packages/core/src/metrics/metric-summary.ts index 2be991297296..f1324def357d 100644 --- a/packages/core/src/metrics/metric-summary.ts +++ b/packages/core/src/metrics/metric-summary.ts @@ -2,7 +2,6 @@ import type { MeasurementUnit, Span } from '@sentry/types'; import type { MetricSummary } from '@sentry/types'; import type { Primitive } from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; -import { getActiveSpan } from '../tracing/utils'; import type { MetricType } from './types'; /** @@ -40,9 +39,10 @@ export function getMetricSummaryJsonForSpan(span: Span): Record, bucketKey: string, ): void { - const span = getActiveSpan(); - if (span) { - const storage = getMetricStorageForSpan(span) || new Map(); + const storage = getMetricStorageForSpan(span) || new Map(); - const exportKey = `${metricType}:${sanitizedName}@${unit}`; - const bucketItem = storage.get(bucketKey); + const exportKey = `${metricType}:${sanitizedName}@${unit}`; + const bucketItem = storage.get(bucketKey); - if (bucketItem) { - const [, summary] = bucketItem; - storage.set(bucketKey, [ - exportKey, - { - min: Math.min(summary.min, value), - max: Math.max(summary.max, value), - count: (summary.count += 1), - sum: (summary.sum += value), - tags: summary.tags, - }, - ]); - } else { - storage.set(bucketKey, [ - exportKey, - { - min: value, - max: value, - count: 1, - sum: value, - tags, - }, - ]); - } - - if (!SPAN_METRIC_SUMMARY) { - SPAN_METRIC_SUMMARY = new WeakMap(); - } + if (bucketItem) { + const [, summary] = bucketItem; + storage.set(bucketKey, [ + exportKey, + { + min: Math.min(summary.min, value), + max: Math.max(summary.max, value), + count: (summary.count += 1), + sum: (summary.sum += value), + tags: summary.tags, + }, + ]); + } else { + storage.set(bucketKey, [ + exportKey, + { + min: value, + max: value, + count: 1, + sum: value, + tags, + }, + ]); + } - SPAN_METRIC_SUMMARY.set(span, storage); + if (!SPAN_METRIC_SUMMARY) { + SPAN_METRIC_SUMMARY = new WeakMap(); } + + SPAN_METRIC_SUMMARY.set(span, storage); } diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 820e41858135..90961cc48bd0 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -81,6 +81,9 @@ export class Scope implements ScopeInterface { /** * Transaction Name + * + * IMPORTANT: The transaction name on the scope has nothing to do with root spans/transaction objects. + * It's purpose is to assign a transaction to the scope that's added to non-transaction events. */ protected _transactionName?: string; @@ -121,7 +124,7 @@ export class Scope implements ScopeInterface { } /** - * Clone this scope instance. + * @inheritDoc */ public clone(): Scope { const newScope = new Scope(); @@ -145,23 +148,22 @@ export class Scope implements ScopeInterface { return newScope; } - /** Update the client on the scope. */ + /** + * @inheritDoc + */ public setClient(client: Client | undefined): void { this._client = client; } /** - * Get the client assigned to this scope. - * - * It is generally recommended to use the global function `Sentry.getClient()` instead, unless you know what you are doing. + * @inheritDoc */ public getClient(): C | undefined { return this._client as C | undefined; } /** - * Add internal on change listener. Used for sub SDKs that need to store the scope. - * @hidden + * @inheritDoc */ public addScopeListener(callback: (scope: Scope) => void): void { this._scopeListeners.push(callback); @@ -279,8 +281,7 @@ export class Scope implements ScopeInterface { } /** - * Sets the transaction name on the scope for future events. - * @deprecated Use extra or tags instead. + * @inheritDoc */ public setTransactionName(name?: string): this { this._transactionName = name; @@ -542,7 +543,7 @@ export class Scope implements ScopeInterface { } /** - * Add data which will be accessible during event processing but won't get sent to Sentry + * @inheritDoc */ public setSDKProcessingMetadata(newData: { [key: string]: unknown }): this { this._sdkProcessingMetadata = { ...this._sdkProcessingMetadata, ...newData }; @@ -566,11 +567,7 @@ export class Scope implements ScopeInterface { } /** - * 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. + * @inheritDoc */ public captureException(exception: unknown, hint?: EventHint): string { const eventId = hint && hint.event_id ? hint.event_id : uuid4(); @@ -597,12 +594,7 @@ export class Scope implements ScopeInterface { } /** - * 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. + * @inheritDoc */ public captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string { const eventId = hint && hint.event_id ? hint.event_id : uuid4(); @@ -630,11 +622,7 @@ export class Scope implements ScopeInterface { } /** - * 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. + * @inheritDoc */ public captureEvent(event: Event, hint?: EventHint): string { const eventId = hint && hint.event_id ? hint.event_id : uuid4(); diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index afd0d123090f..67ad231e7c10 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -19,3 +19,6 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_OP = 'sentry.op'; * Use this attribute to represent the origin of a span. */ export const SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN = 'sentry.origin'; + +/** The reason why an idle span finished. */ +export const SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON = 'sentry.idle_span_finish_reason'; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 682e58e80355..de88e6f3706e 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -24,8 +24,7 @@ import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, } from './tracing'; -import { getRootSpan } from './utils/getRootSpan'; -import { spanToTraceContext } from './utils/spanUtils'; +import { getRootSpan, spanToTraceContext } from './utils/spanUtils'; export interface ServerRuntimeClientOptions extends ClientOptions { platform?: string; @@ -256,8 +255,9 @@ export class ServerRuntimeClient< // eslint-disable-next-line deprecation/deprecation const span = scope.getSpan(); if (span) { - const samplingContext = getRootSpan(span) ? getDynamicSamplingContextFromSpan(span) : undefined; - return [samplingContext, spanToTraceContext(span)]; + const rootSpan = getRootSpan(span); + const samplingContext = getDynamicSamplingContextFromSpan(rootSpan); + return [samplingContext, spanToTraceContext(rootSpan)]; } const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext(); diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index e2dd9af12b0b..04510683a1b9 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -4,8 +4,7 @@ import { dropUndefinedKeys } from '@sentry/utils'; import { DEFAULT_ENVIRONMENT } from '../constants'; import { getClient } from '../currentScopes'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; -import { getRootSpan } from '../utils/getRootSpan'; -import { spanIsSampled, spanToJSON } from '../utils/spanUtils'; +import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; /** * Creates a dynamic sampling context from a client. @@ -50,15 +49,15 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly Global error occured`); - activeTransaction.setStatus({ code: SPAN_STATUS_ERROR, message }); + DEBUG_BUILD && logger.log(`[Tracing] Root span: ${message} -> Global error occured`); + rootSpan.setStatus({ code: SPAN_STATUS_ERROR, message }); } } diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts index 5f30a94498b9..394a2be026fe 100644 --- a/packages/core/src/tracing/hubextensions.ts +++ b/packages/core/src/tracing/hubextensions.ts @@ -1,116 +1,9 @@ -import type { ClientOptions, CustomSamplingContext, Hub, TransactionContext } from '@sentry/types'; -import { getMainCarrier } from '../asyncContext'; - import { registerErrorInstrumentation } from './errors'; -import { IdleTransaction } from './idletransaction'; -import { sampleTransaction } from './sampling'; -import { Transaction } from './transaction'; - -/** - * Creates a new transaction and adds a sampling decision if it doesn't yet have one. - * - * The Hub.startTransaction method delegates to this method to do its work, passing the Hub instance in as `this`, as if - * it had been called on the hub directly. Exists as a separate function so that it can be injected into the class as an - * "extension method." - * - * @param this: The Hub starting the transaction - * @param transactionContext: Data used to configure the transaction - * @param CustomSamplingContext: Optional data to be provided to the `tracesSampler` function (if any) - * - * @returns The new transaction - * - * @see {@link Hub.startTransaction} - */ -function _startTransaction( - this: Hub, - transactionContext: TransactionContext, - customSamplingContext?: CustomSamplingContext, -): Transaction { - // eslint-disable-next-line deprecation/deprecation - const client = this.getClient(); - const options: Partial = (client && client.getOptions()) || {}; - - // eslint-disable-next-line deprecation/deprecation - let transaction = new Transaction(transactionContext, this); - transaction = sampleTransaction(transaction, options, { - name: transactionContext.name, - parentSampled: transactionContext.parentSampled, - transactionContext, - attributes: { - // eslint-disable-next-line deprecation/deprecation - ...transactionContext.data, - ...transactionContext.attributes, - }, - ...customSamplingContext, - }); - if (transaction.isRecording()) { - transaction.initSpanRecorder(); - } - if (client) { - client.emit('startTransaction', transaction); - } - return transaction; -} /** - * Create new idle transaction. - */ -export function startIdleTransaction( - hub: Hub, - transactionContext: TransactionContext, - idleTimeout: number, - finalTimeout: number, - onScope?: boolean, - customSamplingContext?: CustomSamplingContext, - heartbeatInterval?: number, - delayAutoFinishUntilSignal: boolean = false, -): 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, - delayAutoFinishUntilSignal, - ); - transaction = sampleTransaction(transaction, options, { - name: transactionContext.name, - parentSampled: transactionContext.parentSampled, - transactionContext, - attributes: { - // eslint-disable-next-line deprecation/deprecation - ...transactionContext.data, - ...transactionContext.attributes, - }, - ...customSamplingContext, - }); - if (transaction.isRecording()) { - transaction.initSpanRecorder(); - } - if (client) { - client.emit('startTransaction', transaction); - } - return transaction; -} - -/** - * Adds tracing extensions to the global hub. + * Adds tracing extensions. + * TODO (v8): Do we still need this?? Can we solve this differently? */ export function addTracingExtensions(): void { - const carrier = getMainCarrier(); - if (!carrier.__SENTRY__) { - return; - } - carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {}; - if (!carrier.__SENTRY__.extensions.startTransaction) { - carrier.__SENTRY__.extensions.startTransaction = _startTransaction; - } - registerErrorInstrumentation(); } diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts new file mode 100644 index 000000000000..36d1474b39f2 --- /dev/null +++ b/packages/core/src/tracing/idleSpan.ts @@ -0,0 +1,356 @@ +import type { Span, SpanAttributes, StartSpanOptions } from '@sentry/types'; +import { logger, timestampInSeconds } from '@sentry/utils'; +import { getClient, getCurrentScope } from '../currentScopes'; + +import { DEBUG_BUILD } from '../debug-build'; +import { SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON } from '../semanticAttributes'; +import { hasTracingEnabled } from '../utils/hasTracingEnabled'; +import { + getActiveSpan, + getSpanDescendants, + removeChildSpanFromSpan, + spanTimeInputToSeconds, + spanToJSON, +} from '../utils/spanUtils'; +import { SentryNonRecordingSpan } from './sentryNonRecordingSpan'; +import { SPAN_STATUS_ERROR } from './spanstatus'; +import { startInactiveSpan } from './trace'; + +export const TRACING_DEFAULTS = { + idleTimeout: 1_000, + finalTimeout: 30_000, + childSpanTimeout: 15_000, +}; + +const FINISH_REASON_HEARTBEAT_FAILED = 'heartbeatFailed'; +const FINISH_REASON_IDLE_TIMEOUT = 'idleTimeout'; +const FINISH_REASON_FINAL_TIMEOUT = 'finalTimeout'; +const FINISH_REASON_EXTERNAL_FINISH = 'externalFinish'; +const FINISH_REASON_CANCELLED = 'cancelled'; + +// unused +const FINISH_REASON_DOCUMENT_HIDDEN = 'documentHidden'; + +// unusued in this file, but used in BrowserTracing +const FINISH_REASON_INTERRUPTED = 'interactionInterrupted'; + +type IdleSpanFinishReason = + | typeof FINISH_REASON_CANCELLED + | typeof FINISH_REASON_DOCUMENT_HIDDEN + | typeof FINISH_REASON_EXTERNAL_FINISH + | typeof FINISH_REASON_FINAL_TIMEOUT + | typeof FINISH_REASON_HEARTBEAT_FAILED + | typeof FINISH_REASON_IDLE_TIMEOUT + | typeof FINISH_REASON_INTERRUPTED; + +interface IdleSpanOptions { + /** + * The time that has to pass without any span being created. + * If this time is exceeded, the idle span will finish. + */ + idleTimeout: number; + /** + * The max. time an idle span may run. + * If this time is exceeded, the idle span will finish no matter what. + */ + finalTimeout: number; + /** + * The max. time a child span may run. + * If the time since the last span was started exceeds this time, the idle span will finish. + */ + childSpanTimeout?: number; + /** + * When set to `true`, will disable the idle timeout and child timeout + * until the `idleSpanEnableAutoFinish` hook is emitted for the idle span. + * The final timeout mechanism will not be affected by this option, + * meaning the idle span will definitely be finished when the final timeout is + * reached, no matter what this option is configured to. + * + * Defaults to `false`. + */ + disableAutoFinish?: boolean; + /** Allows to configure a hook that is called when the idle span is ended, before it is processed. */ + beforeSpanEnd?: (span: Span) => void; +} + +/** + * An idle span is a span that automatically finishes. It does this by tracking child spans as activities. + * An idle span is always the active span. + */ +export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Partial = {}): Span { + // Activities store a list of active spans + const activities = new Map(); + + // We should not use heartbeat if we finished a span + let _finished = false; + + // Timer that tracks idleTimeout + let _idleTimeoutID: ReturnType | undefined; + + // Timer that tracks maxSpanTime for child spans + let _childSpanTimeoutID: ReturnType | undefined; + + // The reason why the span was finished + let _finishReason: IdleSpanFinishReason = FINISH_REASON_EXTERNAL_FINISH; + + let _autoFinishAllowed: boolean = !options.disableAutoFinish; + + const { + idleTimeout = TRACING_DEFAULTS.idleTimeout, + finalTimeout = TRACING_DEFAULTS.finalTimeout, + childSpanTimeout = TRACING_DEFAULTS.childSpanTimeout, + beforeSpanEnd, + } = options; + + const client = getClient(); + + if (!client || !hasTracingEnabled()) { + return new SentryNonRecordingSpan(); + } + + const scope = getCurrentScope(); + const previousActiveSpan = getActiveSpan(); + const span = _startIdleSpan(startSpanOptions); + + function _endSpan(timestamp: number = timestampInSeconds()): void { + // Ensure we end with the last span timestamp, if possible + const spans = getSpanDescendants(span).filter(child => child !== span); + + // If we have no spans, we just end, nothing else to do here + if (!spans.length) { + span.end(timestamp); + return; + } + + const childEndTimestamps = spans + .map(span => spanToJSON(span).timestamp) + .filter(timestamp => !!timestamp) as number[]; + const latestSpanEndTimestamp = childEndTimestamps.length ? Math.max(...childEndTimestamps) : undefined; + + const spanEndTimestamp = spanTimeInputToSeconds(timestamp); + const spanStartTimestamp = spanToJSON(span).start_timestamp; + + // The final endTimestamp should: + // * Never be before the span start timestamp + // * Be the latestSpanEndTimestamp, if there is one, and it is smaller than the passed span end timestamp + // * Otherwise be the passed end timestamp + const endTimestamp = Math.max( + spanStartTimestamp || -Infinity, + Math.min(spanEndTimestamp, latestSpanEndTimestamp || Infinity), + ); + + span.end(endTimestamp); + } + + /** + * Cancels the existing idle timeout, if there is one. + */ + function _cancelIdleTimeout(): void { + if (_idleTimeoutID) { + clearTimeout(_idleTimeoutID); + _idleTimeoutID = undefined; + } + } + + /** + * Cancels the existing child span timeout, if there is one. + */ + function _cancelChildSpanTimeout(): void { + if (_childSpanTimeoutID) { + clearTimeout(_childSpanTimeoutID); + _childSpanTimeoutID = undefined; + } + } + + /** + * Restarts idle timeout, if there is no running idle timeout it will start one. + */ + function _restartIdleTimeout(endTimestamp?: number): void { + _cancelIdleTimeout(); + _idleTimeoutID = setTimeout(() => { + if (!_finished && activities.size === 0 && _autoFinishAllowed) { + _finishReason = FINISH_REASON_IDLE_TIMEOUT; + _endSpan(endTimestamp); + } + }, idleTimeout); + } + + /** + * Restarts child span timeout, if there is none running it will start one. + */ + function _restartChildSpanTimeout(endTimestamp?: number): void { + _cancelChildSpanTimeout(); + _idleTimeoutID = setTimeout(() => { + if (!_finished && _autoFinishAllowed) { + _finishReason = FINISH_REASON_HEARTBEAT_FAILED; + _endSpan(endTimestamp); + } + }, childSpanTimeout); + } + + /** + * Start tracking a specific activity. + * @param spanId The span id that represents the activity + */ + function _pushActivity(spanId: string): void { + _cancelIdleTimeout(); + activities.set(spanId, true); + DEBUG_BUILD && logger.log(`[Tracing] pushActivity: ${spanId}`); + DEBUG_BUILD && logger.log('[Tracing] new activities count', activities.size); + + const endTimestamp = timestampInSeconds(); + // We need to add the timeout here to have the real endtimestamp of the idle span + // Remember timestampInSeconds is in seconds, timeout is in ms + _restartChildSpanTimeout(endTimestamp + childSpanTimeout / 1000); + } + + /** + * Remove an activity from usage + * @param spanId The span id that represents the activity + */ + function _popActivity(spanId: string): void { + if (activities.has(spanId)) { + DEBUG_BUILD && logger.log(`[Tracing] popActivity ${spanId}`); + activities.delete(spanId); + DEBUG_BUILD && logger.log('[Tracing] new activities count', activities.size); + } + + if (activities.size === 0) { + const endTimestamp = timestampInSeconds(); + // We need to add the timeout here to have the real endtimestamp of the idle span + // Remember timestampInSeconds is in seconds, timeout is in ms + _restartIdleTimeout(endTimestamp + idleTimeout / 1000); + _cancelChildSpanTimeout(); + } + } + + function onIdleSpanEnded(): void { + _finished = true; + activities.clear(); + + if (beforeSpanEnd) { + beforeSpanEnd(span); + } + + // eslint-disable-next-line deprecation/deprecation + scope.setSpan(previousActiveSpan); + + const spanJSON = spanToJSON(span); + + const { timestamp: endTimestamp, start_timestamp: startTimestamp } = spanJSON; + // This should never happen, but to make TS happy... + if (!endTimestamp || !startTimestamp) { + return; + } + + const attributes: SpanAttributes = spanJSON.data || {}; + if (spanJSON.op === 'ui.action.click' && !attributes[SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, _finishReason); + } + + DEBUG_BUILD && + logger.log('[Tracing] finishing idle span', new Date(endTimestamp * 1000).toISOString(), spanJSON.op); + + const childSpans = getSpanDescendants(span).filter(child => child !== span); + + childSpans.forEach(childSpan => { + // We cancel all pending spans with status "cancelled" to indicate the idle span was finished early + if (childSpan.isRecording()) { + childSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'cancelled' }); + childSpan.end(endTimestamp); + DEBUG_BUILD && + logger.log('[Tracing] cancelling span since span ended early', JSON.stringify(childSpan, undefined, 2)); + } + + const childSpanJSON = spanToJSON(childSpan); + const { timestamp: childEndTimestamp = 0, start_timestamp: childStartTimestamp = 0 } = childSpanJSON; + + const spanStartedBeforeIdleSpanEnd = childStartTimestamp <= endTimestamp; + + // Add a delta with idle timeout so that we prevent false positives + const timeoutWithMarginOfError = (finalTimeout + idleTimeout) / 1000; + const spanEndedBeforeFinalTimeout = childEndTimestamp - childStartTimestamp < timeoutWithMarginOfError; + + if (DEBUG_BUILD) { + const stringifiedSpan = JSON.stringify(childSpan, undefined, 2); + if (!spanStartedBeforeIdleSpanEnd) { + logger.log('[Tracing] discarding Span since it happened after idle span was finished', stringifiedSpan); + } else if (!spanEndedBeforeFinalTimeout) { + logger.log('[Tracing] discarding Span since it finished after idle span final timeout', stringifiedSpan); + } + } + + if (!spanEndedBeforeFinalTimeout || !spanStartedBeforeIdleSpanEnd) { + removeChildSpanFromSpan(span, childSpan); + } + }); + + DEBUG_BUILD && logger.log('[Tracing] flushing idle span'); + } + + client.on('spanStart', startedSpan => { + // If we already finished the idle span, + // or if this is the idle span itself being started, + // or if the started span has already been closed, + // we don't care about it for activity + if (_finished || startedSpan === span || !!spanToJSON(startedSpan).timestamp) { + return; + } + + const allSpans = getSpanDescendants(span); + + // If the span that was just started is a child of the idle span, we should track it + if (allSpans.includes(startedSpan)) { + _pushActivity(startedSpan.spanContext().spanId); + } + }); + + client.on('spanEnd', endedSpan => { + if (_finished) { + return; + } + + _popActivity(endedSpan.spanContext().spanId); + + if (endedSpan === span) { + onIdleSpanEnded(); + } + }); + + client.on('idleSpanEnableAutoFinish', spanToAllowAutoFinish => { + if (spanToAllowAutoFinish === span) { + _autoFinishAllowed = true; + _restartIdleTimeout(); + + if (activities.size) { + _restartChildSpanTimeout(); + } + } + }); + + // We only start the initial idle timeout if we are not delaying the auto finish + if (!options.disableAutoFinish) { + _restartIdleTimeout(); + } + + setTimeout(() => { + if (!_finished) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }); + _finishReason = FINISH_REASON_FINAL_TIMEOUT; + _endSpan(); + } + }, finalTimeout); + + return span; +} + +function _startIdleSpan(options: StartSpanOptions): Span { + const span = startInactiveSpan(options); + + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(span); + + DEBUG_BUILD && logger.log(`Setting idle span on scope. Span ID: ${span.spanContext().spanId}`); + + return span; +} diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts deleted file mode 100644 index 0aad33ca6836..000000000000 --- a/packages/core/src/tracing/idletransaction.ts +++ /dev/null @@ -1,418 +0,0 @@ -import type { Hub, SpanTimeInput, TransactionContext } from '@sentry/types'; -import { logger, timestampInSeconds } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../debug-build'; -import { spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; -import type { SentrySpan } from './sentrySpan'; -import { SpanRecorder } from './sentrySpan'; -import { SPAN_STATUS_ERROR } from './spanstatus'; -import { Transaction } from './transaction'; - -export const TRACING_DEFAULTS = { - idleTimeout: 1000, - finalTimeout: 30000, - heartbeatInterval: 5000, -}; - -const FINISH_REASON_TAG = 'finishReason'; - -const IDLE_TRANSACTION_FINISH_REASONS = [ - 'heartbeatFailed', - 'idleTimeout', - 'documentHidden', - 'finalTimeout', - 'externalFinish', - 'cancelled', -]; - -/** - * @inheritDoc - */ -export class IdleTransactionSpanRecorder extends SpanRecorder { - public constructor( - private readonly _pushActivity: (id: string) => void, - private readonly _popActivity: (id: string) => void, - public transactionSpanId: string, - maxlen?: number, - ) { - super(maxlen); - } - - /** - * @inheritDoc - */ - public add(span: SentrySpan): void { - // We should make sure we do not push and pop activities for - // the transaction that this span recorder belongs to. - 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.spanContext().spanId); - return originalEnd.apply(span, rest); - }; - - // We should only push new activities if the span does not have an end timestamp. - if (spanToJSON(span).timestamp === undefined) { - this._pushActivity(span.spanContext().spanId); - } - } - - super.add(span); - } -} - -export type BeforeFinishCallback = (transactionSpan: IdleTransaction, endTimestamp: number) => void; - -/** - * An IdleTransaction is a transaction that automatically finishes. It does this by tracking child spans as activities. - * You can have multiple IdleTransactions active, but if the `onScope` option is specified, the idle transaction will - * put itself on the scope on creation. - */ -export class IdleTransaction extends Transaction { - // Activities store a list of active spans - public activities: Record; - // Track state of activities in previous heartbeat - private _prevHeartbeatString: string | undefined; - - // Amount of times heartbeat has counted. Will cause transaction to finish after 3 beats. - private _heartbeatCounter: number; - - // We should not use heartbeat if we finished a transaction - private _finished: boolean; - - // Idle timeout was canceled and we should finish the transaction with the last span end. - private _idleTimeoutCanceledPermanently: boolean; - - private readonly _beforeFinishCallbacks: BeforeFinishCallback[]; - - /** - * Timer that tracks Transaction idleTimeout - */ - private _idleTimeoutID: ReturnType | undefined; - - private _finishReason: (typeof IDLE_TRANSACTION_FINISH_REASONS)[number]; - - private _autoFinishAllowed: boolean; - - /** - * @deprecated Transactions will be removed in v8. Use spans instead. - */ - public constructor( - transactionContext: TransactionContext, - private readonly _idleHub: Hub, - /** - * The time to wait in ms until the idle transaction will be finished. This timer is started each time - * there are no active spans on this transaction. - */ - private readonly _idleTimeout: number = TRACING_DEFAULTS.idleTimeout, - /** - * The final value in ms that a transaction cannot exceed - */ - private readonly _finalTimeout: number = TRACING_DEFAULTS.finalTimeout, - private readonly _heartbeatInterval: number = TRACING_DEFAULTS.heartbeatInterval, - // Whether or not the transaction should put itself on the scope when it starts and pop itself off when it ends - private readonly _onScope: boolean = false, - /** - * When set to `true`, will disable the idle timeout (`_idleTimeout` option) and heartbeat mechanisms (`_heartbeatInterval` - * option) until the `sendAutoFinishSignal()` method is called. The final timeout mechanism (`_finalTimeout` option) - * will not be affected by this option, meaning the transaction will definitely be finished when the final timeout is - * reached, no matter what this option is configured to. - * - * Defaults to `false`. - */ - delayAutoFinishUntilSignal: boolean = false, - ) { - super(transactionContext, _idleHub); - - this.activities = {}; - this._heartbeatCounter = 0; - this._finished = false; - this._idleTimeoutCanceledPermanently = false; - this._beforeFinishCallbacks = []; - this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[4]; - this._autoFinishAllowed = !delayAutoFinishUntilSignal; - - 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.spanContext().spanId}`); - // eslint-disable-next-line deprecation/deprecation - _idleHub.getScope().setSpan(this); - } - - if (!delayAutoFinishUntilSignal) { - this._restartIdleTimeout(); - } - - setTimeout(() => { - if (!this._finished) { - this.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }); - this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[3]; - this.end(); - } - }, this._finalTimeout); - } - - /** {@inheritDoc} */ - public end(endTimestamp?: SpanTimeInput): string | undefined { - const endTimestampInS = spanTimeInputToSeconds(endTimestamp); - - this._finished = true; - this.activities = {}; - - const op = spanToJSON(this).op; - - if (op === 'ui.action.click') { - this.setAttribute(FINISH_REASON_TAG, this._finishReason); - } - - // eslint-disable-next-line deprecation/deprecation - if (this.spanRecorder) { - DEBUG_BUILD && - logger.log('[Tracing] finishing IdleTransaction', new Date(endTimestampInS * 1000).toISOString(), op); - - for (const callback of this._beforeFinishCallbacks) { - callback(this, endTimestampInS); - } - - // eslint-disable-next-line deprecation/deprecation - this.spanRecorder.spans = this.spanRecorder.spans.filter((span: SentrySpan) => { - // If we are dealing with the transaction itself, we just return it - if (span.spanContext().spanId === this.spanContext().spanId) { - return true; - } - - // We cancel all pending spans with status "cancelled" to indicate the idle transaction was finished early - if (!spanToJSON(span).timestamp) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'cancelled' }); - span.end(endTimestampInS); - DEBUG_BUILD && - logger.log('[Tracing] cancelling span since transaction ended early', JSON.stringify(span, undefined, 2)); - } - - const { start_timestamp: startTime, timestamp: endTime } = spanToJSON(span); - const spanStartedBeforeTransactionFinish = startTime && startTime < endTimestampInS; - - // Add a delta with idle timeout so that we prevent false positives - const timeoutWithMarginOfError = (this._finalTimeout + this._idleTimeout) / 1000; - const spanEndedBeforeFinalTimeout = endTime && startTime && endTime - startTime < timeoutWithMarginOfError; - - if (DEBUG_BUILD) { - const stringifiedSpan = JSON.stringify(span, undefined, 2); - if (!spanStartedBeforeTransactionFinish) { - logger.log('[Tracing] discarding Span since it happened after Transaction was finished', stringifiedSpan); - } else if (!spanEndedBeforeFinalTimeout) { - logger.log('[Tracing] discarding Span since it finished after Transaction final timeout', stringifiedSpan); - } - } - - return spanStartedBeforeTransactionFinish && spanEndedBeforeFinalTimeout; - }); - - DEBUG_BUILD && logger.log('[Tracing] flushing IdleTransaction'); - } else { - DEBUG_BUILD && logger.log('[Tracing] No active IdleTransaction'); - } - - // 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); - } - } - - return super.end(endTimestamp); - } - - /** - * Register a callback function that gets executed before the transaction finishes. - * Useful for cleanup or if you want to add any additional spans based on current context. - * - * This is exposed because users have no other way of running something before an idle transaction - * finishes. - */ - public registerBeforeFinishCallback(callback: BeforeFinishCallback): void { - this._beforeFinishCallbacks.push(callback); - } - - /** - * @inheritDoc - */ - public initSpanRecorder(maxlen?: number): void { - // eslint-disable-next-line deprecation/deprecation - if (!this.spanRecorder) { - const pushActivity = (id: string): void => { - if (this._finished) { - return; - } - this._pushActivity(id); - }; - const popActivity = (id: string): void => { - if (this._finished) { - return; - } - this._popActivity(id); - }; - - // eslint-disable-next-line deprecation/deprecation - 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'); - this._pingHeartbeat(); - } - // eslint-disable-next-line deprecation/deprecation - this.spanRecorder.add(this); - } - - /** - * Cancels the existing idle timeout, if there is one. - * @param restartOnChildSpanChange Default is `true`. - * If set to false the transaction will end - * with the last child span. - */ - public cancelIdleTimeout( - endTimestamp?: Parameters[0], - { - restartOnChildSpanChange, - }: { - restartOnChildSpanChange?: boolean; - } = { - restartOnChildSpanChange: true, - }, - ): void { - this._idleTimeoutCanceledPermanently = restartOnChildSpanChange === false; - if (this._idleTimeoutID) { - clearTimeout(this._idleTimeoutID); - this._idleTimeoutID = undefined; - - if (Object.keys(this.activities).length === 0 && this._idleTimeoutCanceledPermanently) { - this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5]; - this.end(endTimestamp); - } - } - } - - /** - * Temporary method used to externally set the transaction's `finishReason` - * - * ** WARNING** - * This is for the purpose of experimentation only and will be removed in the near future, do not use! - * - * @internal - * - */ - public setFinishReason(reason: string): void { - this._finishReason = reason; - } - - /** - * Permits the IdleTransaction to automatically end itself via the idle timeout and heartbeat mechanisms when the `delayAutoFinishUntilSignal` option was set to `true`. - */ - public sendAutoFinishSignal(): void { - if (!this._autoFinishAllowed) { - DEBUG_BUILD && logger.log('[Tracing] Received finish signal for idle transaction.'); - this._restartIdleTimeout(); - this._autoFinishAllowed = true; - } - } - - /** - * Restarts idle timeout, if there is no running idle timeout it will start one. - */ - private _restartIdleTimeout(endTimestamp?: Parameters[0]): void { - this.cancelIdleTimeout(); - this._idleTimeoutID = setTimeout(() => { - if (!this._finished && Object.keys(this.activities).length === 0) { - this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[1]; - this.end(endTimestamp); - } - }, this._idleTimeout); - } - - /** - * Start tracking a specific activity. - * @param spanId The span id that represents the activity - */ - private _pushActivity(spanId: string): void { - this.cancelIdleTimeout(undefined, { restartOnChildSpanChange: !this._idleTimeoutCanceledPermanently }); - DEBUG_BUILD && logger.log(`[Tracing] pushActivity: ${spanId}`); - this.activities[spanId] = true; - DEBUG_BUILD && logger.log('[Tracing] new activities count', Object.keys(this.activities).length); - } - - /** - * Remove an activity from usage - * @param spanId The span id that represents the activity - */ - private _popActivity(spanId: string): void { - if (this.activities[spanId]) { - DEBUG_BUILD && logger.log(`[Tracing] popActivity ${spanId}`); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.activities[spanId]; - DEBUG_BUILD && logger.log('[Tracing] new activities count', Object.keys(this.activities).length); - } - - if (Object.keys(this.activities).length === 0) { - const endTimestamp = timestampInSeconds(); - if (this._idleTimeoutCanceledPermanently) { - if (this._autoFinishAllowed) { - this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5]; - this.end(endTimestamp); - } - } else { - // We need to add the timeout here to have the real endtimestamp of the transaction - // Remember timestampInSeconds is in seconds, timeout is in ms - this._restartIdleTimeout(endTimestamp + this._idleTimeout / 1000); - } - } - } - - /** - * Checks when entries of this.activities are not changing for 3 beats. - * If this occurs we finish the transaction. - */ - private _beat(): void { - // We should not be running heartbeat if the idle transaction is finished. - if (this._finished) { - return; - } - - const heartbeatString = Object.keys(this.activities).join(''); - - if (heartbeatString === this._prevHeartbeatString) { - this._heartbeatCounter++; - } else { - this._heartbeatCounter = 1; - } - - this._prevHeartbeatString = heartbeatString; - - if (this._heartbeatCounter >= 3) { - if (this._autoFinishAllowed) { - DEBUG_BUILD && logger.log('[Tracing] Transaction finished because of no change for 3 heart beats'); - this.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }); - this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[0]; - this.end(); - } - } else { - this._pingHeartbeat(); - } - } - - /** - * Pings the heartbeat - */ - private _pingHeartbeat(): void { - DEBUG_BUILD && logger.log(`pinging Heartbeat -> current counter: ${this._heartbeatCounter}`); - setTimeout(() => { - this._beat(); - }, this._heartbeatInterval); - } -} diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 9ca8f26eac3f..e6f17a9f8911 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -1,10 +1,10 @@ -export { startIdleTransaction, addTracingExtensions } from './hubextensions'; -export { IdleTransaction, TRACING_DEFAULTS } from './idletransaction'; -export type { BeforeFinishCallback } from './idletransaction'; +export { addTracingExtensions } from './hubextensions'; +export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan'; export { SentrySpan } from './sentrySpan'; +export { SentryNonRecordingSpan } from './sentryNonRecordingSpan'; export { Transaction } from './transaction'; // eslint-disable-next-line deprecation/deprecation -export { getActiveTransaction, getActiveSpan } from './utils'; +export { getActiveTransaction } from './utils'; export { setHttpStatus, getSpanStatusFromHttpCode, @@ -15,6 +15,7 @@ export { startInactiveSpan, startSpanManual, continueTrace, + withActiveSpan, } from './trace'; 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 d050a48d6029..6945bba8aec8 100644 --- a/packages/core/src/tracing/measurement.ts +++ b/packages/core/src/tracing/measurement.ts @@ -1,15 +1,20 @@ -import type { MeasurementUnit } from '@sentry/types'; - -import { getActiveTransaction } from './utils'; +import type { MeasurementUnit, Span, Transaction } from '@sentry/types'; +import { getActiveSpan, getRootSpan } from '../utils/spanUtils'; /** * 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) { + const activeSpan = getActiveSpan(); + const rootSpan = activeSpan && getRootSpan(activeSpan); + + if (rootSpan && rootSpanIsTransaction(rootSpan)) { // eslint-disable-next-line deprecation/deprecation - transaction.setMeasurement(name, value, unit); + rootSpan.setMeasurement(name, value, unit); } } + +function rootSpanIsTransaction(rootSpan: Span): rootSpan is Transaction { + // eslint-disable-next-line deprecation/deprecation + return typeof (rootSpan as Transaction).setMeasurement === 'function'; +} diff --git a/packages/core/src/tracing/sentryNonRecordingSpan.ts b/packages/core/src/tracing/sentryNonRecordingSpan.ts new file mode 100644 index 000000000000..6b86fe4a0fec --- /dev/null +++ b/packages/core/src/tracing/sentryNonRecordingSpan.ts @@ -0,0 +1,62 @@ +import type { + Span, + SpanAttributeValue, + SpanAttributes, + SpanContext, + SpanContextData, + SpanStatus, + SpanTimeInput, +} from '@sentry/types'; +import { uuid4 } from '@sentry/utils'; +import { TRACE_FLAG_NONE } from '../utils/spanUtils'; + +/** + * A Sentry Span that is non-recording, meaning it will not be sent to Sentry. + */ +export class SentryNonRecordingSpan implements Span { + private _traceId: string; + private _spanId: string; + + public constructor(spanContext: SpanContext = {}) { + this._traceId = spanContext.traceId || uuid4(); + this._spanId = spanContext.spanId || uuid4().substring(16); + } + + /** @inheritdoc */ + public spanContext(): SpanContextData { + return { + spanId: this._spanId, + traceId: this._traceId, + traceFlags: TRACE_FLAG_NONE, + }; + } + + /** @inheritdoc */ + // eslint-disable-next-line @typescript-eslint/no-empty-function + public end(_timestamp?: SpanTimeInput): void {} + + /** @inheritdoc */ + public setAttribute(_key: string, _value: SpanAttributeValue | undefined): this { + return this; + } + + /** @inheritdoc */ + public setAttributes(_values: SpanAttributes): this { + return this; + } + + /** @inheritdoc */ + public setStatus(_status: SpanStatus): this { + return this; + } + + /** @inheritdoc */ + public updateName(_name: string): this { + return this; + } + + /** @inheritdoc */ + public isRecording(): boolean { + return false; + } +} diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index b0e82321e33e..87b57d729960 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -1,5 +1,4 @@ import type { - Primitive, Span, SpanAttributeValue, SpanAttributes, @@ -13,63 +12,26 @@ import type { Transaction, } from '@sentry/types'; import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; +import { getClient } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; -import { getRootSpan } from '../utils/getRootSpan'; import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, + addChildSpanToSpan, + getRootSpan, + getStatusMessage, spanTimeInputToSeconds, spanToJSON, spanToTraceContext, } from '../utils/spanUtils'; -import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstatus'; -import { addChildSpanToSpan } from './utils'; - -/** - * Keeps track of finished spans for a given transaction - * @internal - * @hideconstructor - * @hidden - */ -export class SpanRecorder { - public spans: SentrySpan[]; - - private readonly _maxlen: number; - - public constructor(maxlen: number = 1000) { - this._maxlen = maxlen; - this.spans = []; - } - - /** - * This is just so that we don't run out of memory while recording a lot - * of spans. At some point we just stop and flush out the start of the - * trace tree (i.e.the first n spans with the smallest - * start_timestamp). - */ - public add(span: SentrySpan): void { - if (this.spans.length > this._maxlen) { - // eslint-disable-next-line deprecation/deprecation - span.spanRecorder = undefined; - } else { - this.spans.push(span); - } - } -} /** * Span contains all data about a span */ export class SentrySpan implements Span { - /** - * Tags for the span. - * @deprecated Use `spanToJSON(span).atttributes` instead. - */ - public tags: { [key: string]: Primitive }; - /** * Data for the span. * @deprecated Use `spanToJSON(span).atttributes` instead. @@ -77,13 +39,6 @@ export class SentrySpan implements Span { // eslint-disable-next-line @typescript-eslint/no-explicit-any public data: { [key: string]: any }; - /** - * List of spans that were finalized - * - * @deprecated This property will no longer be public. Span recording will be handled internally. - */ - public spanRecorder?: SpanRecorder; - /** * @inheritDoc * @deprecated Use top level `Sentry.getRootSpan()` instead @@ -105,8 +60,8 @@ export class SentrySpan implements Span { private _logMessage?: string; /** - * You should never call the constructor manually, always use `Sentry.startTransaction()` - * or call `startChild()` on an existing span. + * You should never call the constructor manually, always use `Sentry.startSpan()` + * or other span methods. * @internal * @hideconstructor * @hidden @@ -116,13 +71,11 @@ export class SentrySpan implements Span { this._spanId = spanContext.spanId || uuid4().substring(16); this._startTime = spanContext.startTimestamp || timestampInSeconds(); // 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 = {}; this.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanContext.origin || 'manual', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: spanContext.op, ...spanContext.attributes, }); @@ -277,7 +230,7 @@ export class SentrySpan implements Span { * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ public startChild( - spanContext?: Pick>, + spanContext: Pick> = {}, ): Span { const childSpan = new SentrySpan({ ...spanContext, @@ -286,14 +239,6 @@ export class SentrySpan implements Span { traceId: this._traceId, }); - // eslint-disable-next-line deprecation/deprecation - childSpan.spanRecorder = this.spanRecorder; - // eslint-disable-next-line deprecation/deprecation - if (childSpan.spanRecorder) { - // eslint-disable-next-line deprecation/deprecation - childSpan.spanRecorder.add(childSpan); - } - // To allow for interoperability we track the children of a span twice: Once with the span recorder (old) once with // the `addChildSpanToSpan`. Eventually we will only use `addChildSpanToSpan` and drop the span recorder. // To ensure interoperability with the `startSpan` API, `addChildSpanToSpan` is also called here. @@ -315,22 +260,16 @@ export class SentrySpan implements Span { this._logMessage = logMessage; } - return childSpan; - } + const client = getClient(); + if (client) { + client.emit('spanStart', childSpan); + // If it has an endTimestamp, it's already ended + if (spanContext.endTimestamp) { + client.emit('spanEnd', childSpan); + } + } - /** - * 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; + return childSpan; } /** @@ -361,6 +300,18 @@ export class SentrySpan implements Span { Object.keys(attributes).forEach(key => this.setAttribute(key, attributes[key])); } + /** + * This should generally not be used, + * but we need it for browser tracing where we want to adjust the start time afterwards. + * USE THIS WITH CAUTION! + * + * @hidden + * @internal + */ + public updateStartTime(timeInput: SpanTimeInput): void { + this._startTime = spanTimeInputToSeconds(timeInput); + } + /** * @inheritDoc */ @@ -397,6 +348,8 @@ export class SentrySpan implements Span { } this._endTime = spanTimeInputToSeconds(endTimestamp); + + this._onSpanEnded(); } /** @@ -415,8 +368,6 @@ export class SentrySpan implements Span { spanId: this._spanId, startTimestamp: this._startTime, status: this._status, - // eslint-disable-next-line deprecation/deprecation - tags: this.tags, traceId: this._traceId, }); } @@ -447,8 +398,6 @@ export class SentrySpan implements Span { span_id: this._spanId, start_timestamp: this._startTime, status: getStatusMessage(this._status), - // eslint-disable-next-line deprecation/deprecation - tags: Object.keys(this.tags).length > 0 ? this.tags : undefined, timestamp: this._endTime, trace_id: this._traceId, origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, @@ -499,16 +448,12 @@ export class SentrySpan implements Span { return hasData ? data : attributes; } -} - -function getStatusMessage(status: SpanStatus | undefined): string | undefined { - if (!status || status.code === SPAN_STATUS_UNSET) { - return undefined; - } - if (status.code === SPAN_STATUS_OK) { - return 'ok'; + /** Emit `spanEnd` when the span is ended. */ + private _onSpanEnded(): void { + const client = getClient(); + if (client) { + client.emit('spanEnd', this); + } } - - return status.message || 'unknown_error'; } diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 98cd511493c6..2548d94fd60e 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,17 +1,35 @@ -import type { Hub, Scope, Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types'; - -import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; -import { getCurrentScope, getIsolationScope, withScope } from '../currentScopes'; - -import { DEBUG_BUILD } from '../debug-build'; -import { getCurrentHub } from '../hub'; +import type { + ClientOptions, + Hub, + Scope, + Span, + SpanTimeInput, + StartSpanOptions, + TransactionContext, +} from '@sentry/types'; + +import { propagationContextFromHeaders } from '@sentry/utils'; +import type { AsyncContextStrategy } from '../asyncContext'; +import { getMainCarrier } from '../asyncContext'; +import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes'; + +import { getAsyncContextStrategy, getCurrentHub } from '../hub'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; -import { spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; +import { + addChildSpanToSpan, + getActiveSpan, + spanIsSampled, + spanTimeInputToSeconds, + spanToJSON, +} from '../utils/spanUtils'; import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; +import { sampleTransaction } from './sampling'; +import { SentryNonRecordingSpan } from './sentryNonRecordingSpan'; import type { SentrySpan } from './sentrySpan'; import { SPAN_STATUS_ERROR } from './spanstatus'; -import { addChildSpanToSpan, getActiveSpan, setCapturedScopesOnSpan } from './utils'; +import { Transaction } from './transaction'; +import { setCapturedScopesOnSpan } from './utils'; /** * Wraps a function with a transaction/span and finishes the span after the function is done. @@ -24,7 +42,12 @@ import { addChildSpanToSpan, getActiveSpan, setCapturedScopesOnSpan } from './ut * or you didn't set `tracesSampleRate`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startSpan(context: StartSpanOptions, callback: (span: Span | undefined) => T): T { +export function startSpan(context: StartSpanOptions, callback: (span: Span) => T): T { + const acs = getAcs(); + if (acs.startSpan) { + return acs.startSpan(context, callback); + } + const spanContext = normalizeContext(context); return withScope(context.scope, scope => { @@ -35,7 +58,7 @@ export function startSpan(context: StartSpanOptions, callback: (span: Span | const shouldSkipSpan = context.onlyIfParent && !parentSpan; const activeSpan = shouldSkipSpan - ? undefined + ? new SentryNonRecordingSpan() : createChildSpanOrTransaction(hub, { parentSpan, spanContext, @@ -43,23 +66,19 @@ export function startSpan(context: StartSpanOptions, callback: (span: Span | scope, }); - if (activeSpan) { - // eslint-disable-next-line deprecation/deprecation - scope.setSpan(activeSpan); - } + // eslint-disable-next-line deprecation/deprecation + scope.setSpan(activeSpan); return handleCallbackErrors( () => callback(activeSpan), () => { - // Only update the span status if it hasn't been changed yet - if (activeSpan) { - const { status } = spanToJSON(activeSpan); - if (!status || status === 'ok') { - activeSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - } + // Only update the span status if it hasn't been changed yet, and the span is not yet finished + const { status } = spanToJSON(activeSpan); + if (activeSpan.isRecording() && (!status || status === 'ok')) { + activeSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); } }, - () => activeSpan && activeSpan.end(), + () => activeSpan.end(), ); }); } @@ -75,10 +94,12 @@ export function startSpan(context: StartSpanOptions, callback: (span: Span | * or you didn't set `tracesSampleRate`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startSpanManual( - context: StartSpanOptions, - callback: (span: Span | undefined, finish: () => void) => T, -): T { +export function startSpanManual(context: StartSpanOptions, callback: (span: Span, finish: () => void) => T): T { + const acs = getAcs(); + if (acs.startSpanManual) { + return acs.startSpanManual(context, callback); + } + const spanContext = normalizeContext(context); return withScope(context.scope, scope => { @@ -89,7 +110,7 @@ export function startSpanManual( const shouldSkipSpan = context.onlyIfParent && !parentSpan; const activeSpan = shouldSkipSpan - ? undefined + ? new SentryNonRecordingSpan() : createChildSpanOrTransaction(hub, { parentSpan, spanContext, @@ -97,24 +118,20 @@ export function startSpanManual( scope, }); - if (activeSpan) { - // eslint-disable-next-line deprecation/deprecation - scope.setSpan(activeSpan); - } + // eslint-disable-next-line deprecation/deprecation + scope.setSpan(activeSpan); function finishAndSetSpan(): void { - activeSpan && activeSpan.end(); + activeSpan.end(); } return handleCallbackErrors( () => callback(activeSpan, finishAndSetSpan), () => { // Only update the span status if it hasn't been changed yet, and the span is not yet finished - if (activeSpan && activeSpan.isRecording()) { - const { status } = spanToJSON(activeSpan); - if (!status || status === 'ok') { - activeSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - } + const { status } = spanToJSON(activeSpan); + if (activeSpan.isRecording() && (!status || status === 'ok')) { + activeSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); } }, ); @@ -131,9 +148,10 @@ export function startSpanManual( * or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startInactiveSpan(context: StartSpanOptions): Span | undefined { - if (!hasTracingEnabled()) { - return undefined; +export function startInactiveSpan(context: StartSpanOptions): Span { + const acs = getAcs(); + if (acs.startInactiveSpan) { + return acs.startInactiveSpan(context); } const spanContext = normalizeContext(context); @@ -147,7 +165,7 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { const shouldSkipSpan = context.onlyIfParent && !parentSpan; if (shouldSkipSpan) { - return undefined; + return new SentryNonRecordingSpan(); } const scope = context.scope || getCurrentScope(); @@ -160,107 +178,52 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { }); } -interface ContinueTrace { - /** - * Continue a trace from `sentry-trace` and `baggage` values. - * These values can be obtained from incoming request headers, - * or in the browser from `` and `` HTML tags. - * - * @deprecated Use the version of this function taking a callback as second parameter instead: - * - * ``` - * Sentry.continueTrace(sentryTrace: '...', baggage: '...' }, () => { - * // ... - * }) - * ``` - * - */ - ({ - sentryTrace, - baggage, - }: { - // eslint-disable-next-line deprecation/deprecation - sentryTrace: Parameters[0]; - // eslint-disable-next-line deprecation/deprecation - baggage: Parameters[1]; - }): Partial; - - /** - * Continue a trace from `sentry-trace` and `baggage` values. - * These values can be obtained from incoming request headers, or in the browser from `` - * and `` HTML tags. - * - * Spans started with `startSpan`, `startSpanManual` and `startInactiveSpan`, within the callback will automatically - * be attached to the incoming trace. - * - * Deprecation notice: In the next major version of the SDK the provided callback will not receive a transaction - * context argument. - */ - ( - { - sentryTrace, - baggage, - }: { - // eslint-disable-next-line deprecation/deprecation - sentryTrace: Parameters[0]; - // eslint-disable-next-line deprecation/deprecation - baggage: Parameters[1]; - }, - // TODO(v8): Remove parameter from this callback. - callback: (transactionContext: Partial) => V, - ): V; -} - -export const continueTrace: ContinueTrace = ( +/** + * Continue a trace from `sentry-trace` and `baggage` values. + * These values can be obtained from incoming request headers, or in the browser from `` + * and `` HTML tags. + * + * Spans started with `startSpan`, `startSpanManual` and `startInactiveSpan`, within the callback will automatically + * be attached to the incoming trace. + */ +export const continueTrace = ( { sentryTrace, baggage, }: { - // eslint-disable-next-line deprecation/deprecation - sentryTrace: Parameters[0]; - // eslint-disable-next-line deprecation/deprecation - baggage: Parameters[1]; + sentryTrace: Parameters[0]; + baggage: Parameters[1]; }, - callback?: (transactionContext: Partial) => V, -): V | Partial => { - // TODO(v8): Change this function so it doesn't do anything besides setting the propagation context on the current scope: - /* - return withScope((scope) => { - const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); - scope.setPropagationContext(propagationContext); - return callback(); - }) - */ - - const currentScope = getCurrentScope(); - - // eslint-disable-next-line deprecation/deprecation - const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( - sentryTrace, - baggage, - ); - - currentScope.setPropagationContext(propagationContext); - - if (DEBUG_BUILD && traceparentData) { - logger.log(`[Tracing] Continuing trace ${traceparentData.traceId}.`); - } - - const transactionContext: Partial = { - ...traceparentData, - metadata: dropUndefinedKeys({ - dynamicSamplingContext, - }), - }; + callback: () => V, +): V => { + return withScope(scope => { + const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); + scope.setPropagationContext(propagationContext); + return callback(); + }); +}; - if (!callback) { - return transactionContext; +/** + * Forks the current scope and sets the provided span as active span in the context of the provided callback. Can be + * passed `null` to start an entirely new span tree. + * + * @param span Spans started in the context of the provided callback will be children of this span. If `null` is passed, + * spans started within the callback will not be attached to a parent span. + * @param callback Execution context in which the provided span will be active. Is passed the newly forked scope. + * @returns the value returned from the provided callback function. + */ +export function withActiveSpan(span: Span | null, callback: (scope: Scope) => T): T { + const acs = getAcs(); + if (acs.withActiveSpan) { + return acs.withActiveSpan(span, callback); } - return withScope(() => { - return callback(transactionContext); + return withScope(scope => { + // eslint-disable-next-line deprecation/deprecation + scope.setSpan(span || undefined); + return callback(scope); }); -}; +} function createChildSpanOrTransaction( hub: Hub, @@ -275,14 +238,14 @@ function createChildSpanOrTransaction( forceTransaction?: boolean; scope: Scope; }, -): Span | undefined { +): Span { if (!hasTracingEnabled()) { - return undefined; + return new SentryNonRecordingSpan(); } const isolationScope = getIsolationScope(); - let span: Span | undefined; + let span: Span; if (parentSpan && !forceTransaction) { // eslint-disable-next-line deprecation/deprecation span = parentSpan.startChild(spanContext); @@ -293,8 +256,7 @@ function createChildSpanOrTransaction( const { traceId, spanId: parentSpanId } = parentSpan.spanContext(); const sampled = spanIsSampled(parentSpan); - // eslint-disable-next-line deprecation/deprecation - span = hub.startTransaction({ + span = _startTransaction({ traceId, parentSpanId, parentSampled: sampled, @@ -311,8 +273,7 @@ function createChildSpanOrTransaction( ...scope.getPropagationContext(), }; - // eslint-disable-next-line deprecation/deprecation - span = hub.startTransaction({ + span = _startTransaction({ traceId, parentSpanId, parentSampled: sampled, @@ -325,6 +286,13 @@ function createChildSpanOrTransaction( }); } + // TODO v8: Technically `startTransaction` can return undefined, which is not reflected by the types + // This happens if tracing extensions have not been added + // In this case, we just want to return a non-recording span + if (!span) { + return new SentryNonRecordingSpan(); + } + setCapturedScopesOnSpan(span, scope, isolationScope); return span; @@ -347,3 +315,30 @@ function normalizeContext(context: StartSpanOptions): TransactionContext { return context; } + +function getAcs(): AsyncContextStrategy { + const carrier = getMainCarrier(); + return getAsyncContextStrategy(carrier); +} + +function _startTransaction(transactionContext: TransactionContext): Transaction { + const client = getClient(); + const options: Partial = (client && client.getOptions()) || {}; + + // eslint-disable-next-line deprecation/deprecation + let transaction = new Transaction(transactionContext, getCurrentHub()); + transaction = sampleTransaction(transaction, options, { + name: transactionContext.name, + parentSampled: transactionContext.parentSampled, + transactionContext, + attributes: { + // eslint-disable-next-line deprecation/deprecation + ...transactionContext.data, + ...transactionContext.attributes, + }, + }); + if (client) { + client.emit('spanStart', transaction); + } + return transaction; +} diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 9f4194681338..a95fe7c394f9 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -5,6 +5,7 @@ import type { Hub, MeasurementUnit, Measurements, + SpanJSON, SpanTimeInput, Transaction as TransactionInterface, TransactionContext, @@ -18,10 +19,10 @@ import { DEBUG_BUILD } from '../debug-build'; import { getCurrentHub } from '../hub'; import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; -import { spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils'; +import { getSpanDescendants, spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils'; import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; -import { SentrySpan, SpanRecorder } from './sentrySpan'; -import { getCapturedScopesOnSpan, getSpanTree } from './utils'; +import { SentrySpan } from './sentrySpan'; +import { getCapturedScopesOnSpan } from './utils'; /** JSDoc */ export class Transaction extends SentrySpan implements TransactionInterface { @@ -44,8 +45,7 @@ export class Transaction extends SentrySpan implements TransactionInterface { private _metadata: Partial; /** - * This constructor should never be called manually. Those instrumenting tracing should use - * `Sentry.startTransaction()`, and internal methods should use `hub.startTransaction()`. + * This constructor should never be called manually. * @internal * @hideconstructor * @hidden @@ -127,20 +127,6 @@ export class Transaction extends SentrySpan implements TransactionInterface { return this; } - /** - * Attaches SpanRecorder to the span itself - * @param maxlen maximum number of spans that can be recorded - */ - public initSpanRecorder(maxlen: number = 1000): void { - // eslint-disable-next-line deprecation/deprecation - if (!this.spanRecorder) { - // eslint-disable-next-line deprecation/deprecation - this.spanRecorder = new SpanRecorder(maxlen); - } - // eslint-disable-next-line deprecation/deprecation - this.spanRecorder.add(this); - } - /** * Set the context of a transaction event. * @deprecated Use either `.setAttribute()`, or set the context on the scope before creating the transaction. @@ -238,9 +224,6 @@ export class Transaction extends SentrySpan implements TransactionInterface { // eslint-disable-next-line deprecation/deprecation const client = this._hub.getClient(); - if (client) { - client.emit('finishTransaction', this); - } if (this._sampled !== true) { // At this point if `sampled !== true` we want to discard the transaction. @@ -253,8 +236,8 @@ export class Transaction extends SentrySpan implements TransactionInterface { return undefined; } - // We only want to include finished spans in the event - const finishedSpans = getSpanTree(this).filter(span => span !== this && spanToJSON(span).timestamp); + // The transaction span itself should be filtered out + const finishedSpans = getSpanDescendants(this).filter(span => span !== this); if (this._trimEnd && finishedSpans.length > 0) { const endTimes = finishedSpans.map(span => spanToJSON(span).timestamp).filter(Boolean) as number[]; @@ -263,6 +246,13 @@ export class Transaction extends SentrySpan implements TransactionInterface { }); } + // We want to filter out any incomplete SpanJSON objects + function isFullFinishedSpan(input: Partial): input is SpanJSON { + return !!input.start_timestamp && !!input.timestamp && !!input.span_id && !!input.trace_id; + } + + const spans = finishedSpans.map(span => spanToJSON(span)).filter(isFullFinishedSpan); + const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = getCapturedScopesOnSpan(this); // eslint-disable-next-line deprecation/deprecation @@ -276,10 +266,8 @@ export class Transaction extends SentrySpan implements TransactionInterface { // We don't want to override trace context trace: spanToTraceContext(this), }, - spans: finishedSpans.map(span => spanToJSON(span)), + spans, start_timestamp: this._startTime, - // eslint-disable-next-line deprecation/deprecation - tags: this.tags, timestamp: this._endTime, transaction: this._name, type: 'transaction', diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index e01f92a028ba..c22940508138 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -1,7 +1,6 @@ import type { Span, Transaction } from '@sentry/types'; import type { Scope } from '@sentry/types'; import { addNonEnumerableProperty } from '@sentry/utils'; -import { getCurrentScope } from '../currentScopes'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; @@ -23,55 +22,6 @@ export function getActiveTransaction(maybeHub?: Hub): T | // so it can be used in manual instrumentation without necessitating a hard dependency on @sentry/utils export { stripUrlQueryAndFragment } from '@sentry/utils'; -/** - * Returns the currently active span. - */ -export function getActiveSpan(): Span | undefined { - // eslint-disable-next-line deprecation/deprecation - return getCurrentScope().getSpan(); -} - -const CHILD_SPANS_FIELD = '_sentryChildSpans'; - -type SpanWithPotentialChildren = Span & { - [CHILD_SPANS_FIELD]?: Set; -}; - -/** - * Adds an opaque child span reference to a span. - */ -export function addChildSpanToSpan(span: SpanWithPotentialChildren, childSpan: Span): void { - if (span[CHILD_SPANS_FIELD] && span[CHILD_SPANS_FIELD].size < 1000) { - span[CHILD_SPANS_FIELD].add(childSpan); - } else { - span[CHILD_SPANS_FIELD] = new Set([childSpan]); - } -} - -/** - * Obtains the entire span tree, meaning a span + all of its descendants for a particular span. - */ -export function getSpanTree(span: SpanWithPotentialChildren): Span[] { - const resultSet = new Set(); - - function addSpanChildren(span: SpanWithPotentialChildren): void { - // This exit condition is required to not infinitely loop in case of a circular dependency. - if (resultSet.has(span)) { - return; - } else { - resultSet.add(span); - const childSpans = span[CHILD_SPANS_FIELD] ? Array.from(span[CHILD_SPANS_FIELD]) : []; - for (const childSpan of childSpans) { - addSpanChildren(childSpan); - } - } - } - - addSpanChildren(span); - - return Array.from(resultSet); -} - const SCOPE_ON_START_SPAN_FIELD = '_sentryScope'; const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope'; diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index 2d0d20661fe2..b41aa2bb4818 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -1,8 +1,7 @@ import type { Breadcrumb, Event, ScopeData, Span } from '@sentry/types'; import { arrayify, dropUndefinedKeys } from '@sentry/utils'; import { getDynamicSamplingContextFromSpan } from '../tracing/dynamicSamplingContext'; -import { getRootSpan } from './getRootSpan'; -import { spanToJSON, spanToTraceContext } from './spanUtils'; +import { getRootSpan, spanToJSON, spanToTraceContext } from './spanUtils'; /** * Applies data from the scope to the event and runs all event processors on it. @@ -39,7 +38,6 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { eventProcessors, attachments, propagationContext, - // eslint-disable-next-line deprecation/deprecation transactionName, span, } = mergeData; @@ -55,7 +53,6 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { } if (transactionName) { - // eslint-disable-next-line deprecation/deprecation data.transactionName = transactionName; } @@ -119,15 +116,7 @@ export function mergeArray( } function applyDataToEvent(event: Event, data: ScopeData): void { - const { - extra, - tags, - user, - contexts, - level, - // eslint-disable-next-line deprecation/deprecation - transactionName, - } = data; + const { extra, tags, user, contexts, level, transactionName } = data; const cleanedExtra = dropUndefinedKeys(extra); if (cleanedExtra && Object.keys(cleanedExtra).length) { @@ -153,7 +142,8 @@ function applyDataToEvent(event: Event, data: ScopeData): void { event.level = level; } - if (transactionName) { + // transaction events get their `transaction` from the root span name + if (transactionName && event.type !== 'transaction') { event.transaction = transactionName; } } @@ -172,17 +162,16 @@ function applySdkMetadataToEvent(event: Event, sdkProcessingMetadata: ScopeData[ function applySpanToEvent(event: Event, span: Span): void { event.contexts = { trace: spanToTraceContext(span), ...event.contexts }; + + event.sdkProcessingMetadata = { + dynamicSamplingContext: getDynamicSamplingContextFromSpan(span), + ...event.sdkProcessingMetadata, + }; + const rootSpan = getRootSpan(span); - if (rootSpan) { - event.sdkProcessingMetadata = { - dynamicSamplingContext: getDynamicSamplingContextFromSpan(span), - ...event.sdkProcessingMetadata, - }; - - const transactionName = spanToJSON(rootSpan).description; - if (transactionName && !event.transaction) { - event.transaction = transactionName; - } + const transactionName = spanToJSON(rootSpan).description; + if (transactionName && !event.transaction && event.type === 'transaction') { + event.transaction = transactionName; } } diff --git a/packages/core/src/utils/getRootSpan.ts b/packages/core/src/utils/getRootSpan.ts deleted file mode 100644 index fe6274c60670..000000000000 --- a/packages/core/src/utils/getRootSpan.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Span } from '@sentry/types'; -import type { SentrySpan } from './../tracing/sentrySpan'; - -/** - * 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 as SentrySpan).transaction ? (span as SentrySpan).transaction : undefined; -} diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 436d383b8a7c..094f6674121c 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -1,6 +1,28 @@ -import type { Span, SpanJSON, SpanTimeInput, TraceContext } from '@sentry/types'; -import { dropUndefinedKeys, generateSentryTraceHeader, timestampInSeconds } from '@sentry/utils'; +import type { + MeasurementUnit, + Primitive, + Span, + SpanAttributes, + SpanJSON, + SpanOrigin, + SpanStatus, + SpanTimeInput, + TraceContext, +} from '@sentry/types'; +import { + addNonEnumerableProperty, + dropUndefinedKeys, + generateSentryTraceHeader, + timestampInSeconds, +} from '@sentry/utils'; +import { getMainCarrier } from '../asyncContext'; +import { getCurrentScope } from '../currentScopes'; +import { getAsyncContextStrategy } from '../hub'; +import { getMetricSummaryJsonForSpan, updateMetricSummaryOnSpan } from '../metrics/metric-summary'; +import type { MetricType } from '../metrics/types'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { SentrySpan } from '../tracing/sentrySpan'; +import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; // These are aligned with OpenTelemetry trace flags export const TRACE_FLAG_NONE = 0x0; @@ -11,7 +33,7 @@ export const TRACE_FLAG_SAMPLED = 0x1; */ export function spanToTraceContext(span: Span): TraceContext { const { spanId: span_id, traceId: trace_id } = span.spanContext(); - const { data, op, parent_span_id, status, tags, origin } = spanToJSON(span); + const { data, op, parent_span_id, status, origin } = spanToJSON(span); return dropUndefinedKeys({ data, @@ -19,7 +41,6 @@ export function spanToTraceContext(span: Span): TraceContext { parent_span_id, span_id, status, - tags, trace_id, origin, }); @@ -62,8 +83,6 @@ function ensureTimestampInSeconds(timestamp: number): number { return isMs ? timestamp / 1000 : timestamp; } -type SpanWithToJSON = Span & { toJSON: () => SpanJSON }; - /** * Convert a span to a JSON representation. * Note that all fields returned here are optional and need to be guarded against. @@ -77,14 +96,52 @@ export function spanToJSON(span: Span): Partial { return span.getSpanJSON(); } - // Fallback: We also check for `.toJSON()` here... - if (typeof (span as SpanWithToJSON).toJSON === 'function') { - return (span as SpanWithToJSON).toJSON(); + try { + const { spanId: span_id, traceId: trace_id } = span.spanContext(); + + // Handle a span from @opentelemetry/sdk-base-trace's `Span` class + if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) { + const { attributes, startTime, name, endTime, parentSpanId, status } = span; + + return dropUndefinedKeys({ + span_id, + trace_id, + data: attributes, + description: name, + parent_span_id: parentSpanId, + start_timestamp: spanTimeInputToSeconds(startTime), + // This is [0,0] by default in OTEL, in which case we want to interpret this as no end time + timestamp: spanTimeInputToSeconds(endTime) || undefined, + status: getStatusMessage(status), + op: attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], + origin: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, + _metrics_summary: getMetricSummaryJsonForSpan(span), + }); + } + + // Finally, at least we have `spanContext()`.... + return { + span_id, + trace_id, + }; + } catch { + return {}; } +} - // TODO: Also handle OTEL spans here! +function spanIsOpenTelemetrySdkTraceBaseSpan(span: Span): span is OpenTelemetrySdkTraceBaseSpan { + const castSpan = span as OpenTelemetrySdkTraceBaseSpan; + return !!castSpan.attributes && !!castSpan.startTime && !!castSpan.name && !!castSpan.endTime && !!castSpan.status; +} - return {}; +/** Exported only for tests. */ +export interface OpenTelemetrySdkTraceBaseSpan extends Span { + attributes: SpanAttributes; + startTime: SpanTimeInput; + name: string; + status: SpanStatus; + endTime: SpanTimeInput; + parentSpanId?: string; } /** @@ -105,6 +162,114 @@ 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); + return traceFlags === TRACE_FLAG_SAMPLED; +} + +/** Get the status message to use for a JSON representation of a span. */ +export function getStatusMessage(status: SpanStatus | undefined): string | undefined { + if (!status || status.code === SPAN_STATUS_UNSET) { + return undefined; + } + + if (status.code === SPAN_STATUS_OK) { + return 'ok'; + } + + return status.message || 'unknown_error'; +} + +const CHILD_SPANS_FIELD = '_sentryChildSpans'; +const ROOT_SPAN_FIELD = '_sentryRootSpan'; + +type SpanWithPotentialChildren = Span & { + [CHILD_SPANS_FIELD]?: Set; + [ROOT_SPAN_FIELD]?: Span; +}; + +/** + * Adds an opaque child span reference to a span. + */ +export function addChildSpanToSpan(span: SpanWithPotentialChildren, childSpan: Span): void { + // We store the root span reference on the child span + // We need this for `getRootSpan()` to work + const rootSpan = span[ROOT_SPAN_FIELD] || span; + addNonEnumerableProperty(childSpan as SpanWithPotentialChildren, ROOT_SPAN_FIELD, rootSpan); + + // We store a list of child spans on the parent span + // We need this for `getSpanDescendants()` to work + if (span[CHILD_SPANS_FIELD] && span[CHILD_SPANS_FIELD].size < 1000) { + span[CHILD_SPANS_FIELD].add(childSpan); + } else { + addNonEnumerableProperty(span, CHILD_SPANS_FIELD, new Set([childSpan])); + } +} + +/** This is only used internally by Idle Spans. */ +export function removeChildSpanFromSpan(span: SpanWithPotentialChildren, childSpan: Span): void { + if (span[CHILD_SPANS_FIELD]) { + span[CHILD_SPANS_FIELD].delete(childSpan); + } +} + +/** + * Returns an array of the given span and all of its descendants. + */ +export function getSpanDescendants(span: SpanWithPotentialChildren): Span[] { + const resultSet = new Set(); + + function addSpanChildren(span: SpanWithPotentialChildren): void { + // This exit condition is required to not infinitely loop in case of a circular dependency. + if (resultSet.has(span)) { + return; + // We want to ignore unsampled spans (e.g. non recording spans) + } else if (spanIsSampled(span)) { + resultSet.add(span); + const childSpans = span[CHILD_SPANS_FIELD] ? Array.from(span[CHILD_SPANS_FIELD]) : []; + for (const childSpan of childSpans) { + addSpanChildren(childSpan); + } + } + } + + addSpanChildren(span); + + return Array.from(resultSet); +} + +/** + * Returns the root span of a given span. + */ +export function getRootSpan(span: SpanWithPotentialChildren): Span { + return span[ROOT_SPAN_FIELD] || span; +} + +/** + * Returns the currently active span. + */ +export function getActiveSpan(): Span | undefined { + const carrier = getMainCarrier(); + const acs = getAsyncContextStrategy(carrier); + if (acs.getActiveSpan) { + return acs.getActiveSpan(); + } + + // eslint-disable-next-line deprecation/deprecation + return getCurrentScope().getSpan(); +} + +/** + * Updates the metric summary on the currently active span + */ +export function updateMetricSummaryOnActiveSpan( + metricType: MetricType, + sanitizedName: string, + value: number, + unit: MeasurementUnit, + tags: Record, + bucketKey: string, +): void { + const span = getActiveSpan(); + if (span) { + updateMetricSummaryOnSpan(span, metricType, sanitizedName, value, unit, tags, bucketKey); + } } diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index b85df791af9b..6c7c8aa0d63d 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -1,19 +1,12 @@ -import type { Client, Envelope, Event, Transaction } from '@sentry/types'; +import type { Client, Envelope, Event } from '@sentry/types'; import { SentryError, SyncPromise, dsnToString, logger } from '@sentry/utils'; -import { - Scope, - addBreadcrumb, - getCurrentScope, - getIsolationScope, - makeSession, - setCurrentClient, - setGlobalScope, -} from '../../src'; +import { Scope, addBreadcrumb, getCurrentScope, getIsolationScope, makeSession, setCurrentClient } from '../../src'; import * as integrationModule from '../../src/integration'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; import { AdHocIntegration, TestIntegration } from '../mocks/integration'; import { makeFakeTransport } from '../mocks/transport'; +import { clearGlobalScope } from './clear-global-scope'; const PUBLIC_DSN = 'https://username@domain/123'; // eslint-disable-next-line no-var @@ -62,7 +55,7 @@ describe('BaseClient', () => { beforeEach(() => { TestClient.sendEventCalled = undefined; TestClient.instance = undefined; - setGlobalScope(undefined); + clearGlobalScope(); getCurrentScope().clear(); getCurrentScope().setClient(undefined); getIsolationScope().clear(); @@ -1935,20 +1928,6 @@ describe('BaseClient', () => { ] as const; describe.each(scenarios)('with client %s', (_, client) => { - it('should call a startTransaction hook', () => { - expect.assertions(1); - - const mockTransaction = { - traceId: '86f39e84263a4de99c326acab3bfe3bd', - } as Transaction; - - client.on('startTransaction', transaction => { - expect(transaction).toEqual(mockTransaction); - }); - - client.emit('startTransaction', mockTransaction); - }); - it('should call a beforeEnvelope hook', () => { expect.assertions(1); diff --git a/packages/core/test/lib/clear-global-scope.ts b/packages/core/test/lib/clear-global-scope.ts new file mode 100644 index 000000000000..5290a610e961 --- /dev/null +++ b/packages/core/test/lib/clear-global-scope.ts @@ -0,0 +1,6 @@ +import { GLOBAL_OBJ } from '@sentry/utils'; + +export function clearGlobalScope() { + const __SENTRY__ = (GLOBAL_OBJ.__SENTRY__ = GLOBAL_OBJ.__SENTRY__ || {}); + __SENTRY__.globalScope = undefined; +} diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index 1ad80fb5ce68..49dcc349f2ed 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 { getGlobalScope, getIsolationScope, setGlobalScope } from '../../src'; +import { getGlobalScope, getIsolationScope } from '../../src'; import { Scope } from '../../src/scope'; import { @@ -18,6 +18,7 @@ import { parseEventHintOrCaptureContext, prepareEvent, } from '../../src/utils/prepareEvent'; +import { clearGlobalScope } from './clear-global-scope'; describe('applyDebugIds', () => { afterEach(() => { @@ -191,7 +192,7 @@ describe('parseEventHintOrCaptureContext', () => { describe('prepareEvent', () => { beforeEach(() => { - setGlobalScope(undefined); + clearGlobalScope(); getIsolationScope().clear(); }); diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index 48a39d7f09b0..978ebde52c9f 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -1,26 +1,19 @@ import type { Attachment, Breadcrumb, Client, Event, RequestSessionStatus } from '@sentry/types'; import { - addTracingExtensions, applyScopeDataToEvent, - getActiveSpan, getCurrentScope, getGlobalScope, getIsolationScope, - setCurrentClient, - setGlobalScope, - spanToJSON, - startInactiveSpan, - startSpan, withIsolationScope, } from '../../src'; -import { withActiveSpan } from '../../src/exports'; import { Scope } from '../../src/scope'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; +import { clearGlobalScope } from './clear-global-scope'; describe('Scope', () => { beforeEach(() => { - setGlobalScope(undefined); + clearGlobalScope(); }); it('allows to create & update a scope', () => { @@ -498,7 +491,7 @@ describe('Scope', () => { describe('global scope', () => { beforeEach(() => { - setGlobalScope(undefined); + clearGlobalScope(); }); it('works', () => { @@ -937,48 +930,3 @@ describe('isolation scope', () => { }); }); }); - -describe('withActiveSpan()', () => { - beforeAll(() => { - addTracingExtensions(); - }); - - beforeEach(() => { - const options = getDefaultTestClientOptions({ enableTracing: true }); - const client = new TestClient(options); - setCurrentClient(client); - client.init(); - }); - - it('should set the active span within the callback', () => { - expect.assertions(2); - const inactiveSpan = startInactiveSpan({ name: 'inactive-span' }); - - expect(getActiveSpan()).not.toBe(inactiveSpan); - - withActiveSpan(inactiveSpan!, () => { - expect(getActiveSpan()).toBe(inactiveSpan); - }); - }); - - it('should create child spans when calling startSpan within the callback', () => { - const inactiveSpan = startInactiveSpan({ name: 'inactive-span' }); - - const parentSpanId = withActiveSpan(inactiveSpan!, () => { - return startSpan({ name: 'child-span' }, childSpan => { - return spanToJSON(childSpan!).parent_span_id; - }); - }); - - expect(parentSpanId).toBe(inactiveSpan?.spanContext().spanId); - }); - - it('when `null` is passed, no span should be active within the callback', () => { - expect.assertions(1); - startSpan({ name: 'parent-span' }, () => { - withActiveSpan(null, () => { - expect(getActiveSpan()).toBeUndefined(); - }); - }); - }); -}); diff --git a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts index 9c531907eac3..f38d3f3e26de 100644 --- a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts +++ b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts @@ -4,8 +4,12 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setCurrentClient, } from '../../../src'; -import { Transaction, getDynamicSamplingContextFromSpan, startInactiveSpan } from '../../../src/tracing'; -import { addTracingExtensions } from '../../../src/tracing'; +import { + Transaction, + addTracingExtensions, + getDynamicSamplingContextFromSpan, + startInactiveSpan, +} from '../../../src/tracing'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; describe('getDynamicSamplingContextFromSpan', () => { diff --git a/packages/core/test/lib/tracing/errors.test.ts b/packages/core/test/lib/tracing/errors.test.ts index 24c19a24121d..76c4aa9609f2 100644 --- a/packages/core/test/lib/tracing/errors.test.ts +++ b/packages/core/test/lib/tracing/errors.test.ts @@ -68,7 +68,7 @@ describe('registerErrorHandlers()', () => { startSpan({ name: 'test' }, span => { mockErrorCallback({} as HandlerDataError); - expect(spanToJSON(span!).status).toBe('internal_error'); + expect(spanToJSON(span).status).toBe('internal_error'); }); }); @@ -77,7 +77,7 @@ describe('registerErrorHandlers()', () => { startSpan({ name: 'test' }, span => { mockUnhandledRejectionCallback({}); - expect(spanToJSON(span!).status).toBe('internal_error'); + expect(spanToJSON(span).status).toBe('internal_error'); }); }); }); diff --git a/packages/core/test/lib/tracing/idleSpan.test.ts b/packages/core/test/lib/tracing/idleSpan.test.ts new file mode 100644 index 000000000000..a0d5b5bf3123 --- /dev/null +++ b/packages/core/test/lib/tracing/idleSpan.test.ts @@ -0,0 +1,541 @@ +import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; + +import type { Event, Span } from '@sentry/types'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, + SentryNonRecordingSpan, + SentrySpan, + addTracingExtensions, + getActiveSpan, + getClient, + getCurrentScope, + getGlobalScope, + getIsolationScope, + setCurrentClient, + spanToJSON, + startInactiveSpan, + startSpan, + startSpanManual, +} from '../../../src'; +import { TRACING_DEFAULTS, startIdleSpan } from '../../../src/tracing/idleSpan'; + +const dsn = 'https://123@sentry.io/42'; + +describe('startIdleSpan', () => { + beforeEach(() => { + jest.useFakeTimers(); + addTracingExtensions(); + + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + + const options = getDefaultTestClientOptions({ dsn, tracesSampleRate: 1 }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('sets & unsets the idle span on the scope', () => { + const idleSpan = startIdleSpan({ name: 'foo' }); + expect(idleSpan).toBeDefined(); + expect(idleSpan).toBeInstanceOf(SentrySpan); + + expect(getActiveSpan()).toBe(idleSpan); + + idleSpan!.end(); + jest.runAllTimers(); + + expect(getActiveSpan()).toBe(undefined); + }); + + it('returns non recording span if tracing is disabled', () => { + const options = getDefaultTestClientOptions({ dsn }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + + const idleSpan = startIdleSpan({ name: 'foo' }); + expect(idleSpan).toBeDefined(); + expect(idleSpan).toBeInstanceOf(SentryNonRecordingSpan); + + // not set as active span, though + expect(getActiveSpan()).toBe(undefined); + }); + + it('does not finish idle span if there are still active activities', () => { + const idleSpan = startIdleSpan({ name: 'foo' }); + expect(idleSpan).toBeDefined(); + + startSpanManual({ name: 'inner1' }, span => { + const childSpan = startInactiveSpan({ name: 'inner2' }); + + span?.end(); + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); + + // Idle span is still recording + expect(idleSpan.isRecording()).toBe(true); + + childSpan?.end(); + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); + + // Now it is finished! + expect(idleSpan.isRecording()).toBe(false); + }); + }); + + it('calls beforeSpanEnd callback before finishing', () => { + const beforeSpanEnd = jest.fn(); + const idleSpan = startIdleSpan({ name: 'foo' }, { beforeSpanEnd }); + expect(idleSpan).toBeDefined(); + + expect(beforeSpanEnd).not.toHaveBeenCalled(); + + startSpan({ name: 'inner' }, () => {}); + + jest.runOnlyPendingTimers(); + expect(beforeSpanEnd).toHaveBeenCalledTimes(1); + expect(beforeSpanEnd).toHaveBeenLastCalledWith(idleSpan); + }); + + it('allows to mutate idle span in beforeSpanEnd before it is sent', () => { + const transactions: Event[] = []; + const beforeSendTransaction = jest.fn(event => { + transactions.push(event); + return null; + }); + const options = getDefaultTestClientOptions({ + dsn, + tracesSampleRate: 1, + beforeSendTransaction, + }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // We want to accomodate a bit of drift there, so we ensure this starts earlier... + const baseTimeInSeconds = Math.floor(Date.now() / 1000) - 9999; + + const beforeSpanEnd = jest.fn((span: Span) => { + span.setAttribute('foo', 'bar'); + // Try adding a child here - we do this in browser tracing... + const inner = startInactiveSpan({ name: 'from beforeSpanEnd', startTime: baseTimeInSeconds }); + inner?.end(baseTimeInSeconds); + }); + const idleSpan = startIdleSpan({ name: 'idle span 2', startTime: baseTimeInSeconds }, { beforeSpanEnd }); + expect(idleSpan).toBeDefined(); + + expect(beforeSpanEnd).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); + jest.runOnlyPendingTimers(); + + expect(spanToJSON(idleSpan!).data).toEqual( + expect.objectContaining({ + foo: 'bar', + }), + ); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + const transaction = transactions[0]; + + expect(transaction.contexts?.trace?.data).toEqual( + expect.objectContaining({ + foo: 'bar', + }), + ); + expect(transaction.spans).toHaveLength(1); + expect(transaction.spans).toEqual([expect.objectContaining({ description: 'from beforeSpanEnd' })]); + }); + + it('filters spans on end', () => { + const transactions: Event[] = []; + const beforeSendTransaction = jest.fn(event => { + transactions.push(event); + return null; + }); + const options = getDefaultTestClientOptions({ + dsn, + tracesSampleRate: 1, + beforeSendTransaction, + }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // We want to accomodate a bit of drift there, so we ensure this starts earlier... + const baseTimeInSeconds = Math.floor(Date.now() / 1000) - 9999; + + const idleSpan = startIdleSpan({ name: 'idle span', startTime: baseTimeInSeconds }); + expect(idleSpan).toBeDefined(); + + // regular child - should be kept + const regularSpan = startInactiveSpan({ + name: 'regular span', + startTime: baseTimeInSeconds + 2, + }); + + // discardedSpan - startTimestamp is too large + const discardedSpan = startInactiveSpan({ name: 'discarded span', startTime: baseTimeInSeconds + 99 }); + // discardedSpan2 - endTime is too large + const discardedSpan2 = startInactiveSpan({ name: 'discarded span', startTime: baseTimeInSeconds + 3 }); + discardedSpan2.end(baseTimeInSeconds + 99)!; + + // Should be cancelled - will not finish + const cancelledSpan = startInactiveSpan({ + name: 'cancelled span', + startTime: baseTimeInSeconds + 4, + }); + + regularSpan.end(baseTimeInSeconds + 4); + idleSpan.end(baseTimeInSeconds + 10); + + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); + jest.runOnlyPendingTimers(); + + expect(regularSpan.isRecording()).toBe(false); + expect(idleSpan.isRecording()).toBe(false); + expect(discardedSpan.isRecording()).toBe(false); + expect(discardedSpan2.isRecording()).toBe(false); + expect(cancelledSpan.isRecording()).toBe(false); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + const transaction = transactions[0]; + + // End time is based on idle time etc. + const idleSpanEndTime = transaction.timestamp!; + expect(idleSpanEndTime).toEqual(expect.any(Number)); + + expect(transaction.spans).toHaveLength(2); + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'regular span', + timestamp: baseTimeInSeconds + 4, + start_timestamp: baseTimeInSeconds + 2, + }), + ]), + ); + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'cancelled span', + timestamp: idleSpanEndTime, + start_timestamp: baseTimeInSeconds + 4, + status: 'cancelled', + }), + ]), + ); + }); + + it('emits span hooks', () => { + const client = getClient()!; + + const hookSpans: { span: Span; hook: string }[] = []; + client.on('spanStart', span => { + hookSpans.push({ span, hook: 'spanStart' }); + }); + client.on('spanEnd', span => { + hookSpans.push({ span, hook: 'spanEnd' }); + }); + + const idleSpan = startIdleSpan({ name: 'idle span' }); + expect(idleSpan).toBeDefined(); + + expect(hookSpans).toEqual([{ span: idleSpan, hook: 'spanStart' }]); + + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeDefined(); + + expect(hookSpans).toEqual([ + { span: idleSpan, hook: 'spanStart' }, + { span: idleSpan, hook: 'spanEnd' }, + ]); + }); + + it('should record dropped idle span', () => { + const options = getDefaultTestClientOptions({ + dsn, + tracesSampleRate: 0, + }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + + const recordDroppedEventSpy = jest.spyOn(client, 'recordDroppedEvent'); + + const idleSpan = startIdleSpan({ name: 'idle span' }); + expect(idleSpan).toBeDefined(); + + idleSpan?.end(); + + expect(recordDroppedEventSpy).toHaveBeenCalledWith('sample_rate', 'transaction'); + }); + + it('sets finish reason when span ends', () => { + let transaction: Event | undefined; + const beforeSendTransaction = jest.fn(event => { + transaction = event; + return null; + }); + const options = getDefaultTestClientOptions({ dsn, tracesSampleRate: 1, beforeSendTransaction }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // This is only set when op === 'ui.action.click' + startIdleSpan({ name: 'foo', op: 'ui.action.click' }); + startSpan({ name: 'inner' }, () => {}); + jest.runOnlyPendingTimers(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + expect(transaction?.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]).toEqual( + 'idleTimeout', + ); + }); + + it('uses finish reason set outside when span ends', () => { + let transaction: Event | undefined; + const beforeSendTransaction = jest.fn(event => { + transaction = event; + return null; + }); + const options = getDefaultTestClientOptions({ dsn, tracesSampleRate: 1, beforeSendTransaction }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // This is only set when op === 'ui.action.click' + const span = startIdleSpan({ name: 'foo', op: 'ui.action.click' }); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, 'custom reason'); + startSpan({ name: 'inner' }, () => {}); + jest.runOnlyPendingTimers(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + expect(transaction?.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]).toEqual( + 'custom reason', + ); + }); + + describe('idleTimeout', () => { + it('finishes if no activities are added to the idle span', () => { + const idleSpan = startIdleSpan({ name: 'idle span' }); + expect(idleSpan).toBeDefined(); + + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeDefined(); + }); + + it('does not finish if a activity is started', () => { + const idleSpan = startIdleSpan({ name: 'idle span' }); + expect(idleSpan).toBeDefined(); + + startInactiveSpan({ name: 'span' }); + + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + }); + + it('does not finish when idleTimeout is not exceed after last activity finished', () => { + const idleTimeout = 10; + const idleSpan = startIdleSpan({ name: 'idle span' }, { idleTimeout }); + expect(idleSpan).toBeDefined(); + + startSpan({ name: 'span1' }, () => {}); + + jest.advanceTimersByTime(2); + + startSpan({ name: 'span2' }, () => {}); + + jest.advanceTimersByTime(8); + + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + }); + + it('finish when idleTimeout is exceeded after last activity finished', () => { + const idleTimeout = 10; + const idleSpan = startIdleSpan({ name: 'idle span', startTime: 1234 }, { idleTimeout }); + expect(idleSpan).toBeDefined(); + + startSpan({ name: 'span1' }, () => {}); + + jest.advanceTimersByTime(2); + + startSpan({ name: 'span2' }, () => {}); + + jest.advanceTimersByTime(10); + + expect(spanToJSON(idleSpan).timestamp).toBeDefined(); + }); + }); + + describe('child span timeout', () => { + it('finishes when a child span exceed timeout', () => { + const idleSpan = startIdleSpan({ name: 'idle span' }); + expect(idleSpan).toBeDefined(); + + // Start any span to cancel idle timeout + startInactiveSpan({ name: 'span' }); + + expect(spanToJSON(idleSpan).status).not.toEqual('deadline_exceeded'); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + // Wait some time + jest.advanceTimersByTime(TRACING_DEFAULTS.childSpanTimeout - 1000); + expect(spanToJSON(idleSpan).status).not.toEqual('deadline_exceeded'); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + // Wait for timeout to exceed + jest.advanceTimersByTime(1000); + expect(spanToJSON(idleSpan).status).not.toEqual('deadline_exceeded'); + expect(spanToJSON(idleSpan).timestamp).toBeDefined(); + }); + + it('resets after new activities are added', () => { + const idleSpan = startIdleSpan({ name: 'idle span' }, { finalTimeout: 99_999 }); + expect(idleSpan).toBeDefined(); + + // Start any span to cancel idle timeout + startInactiveSpan({ name: 'span' }); + + expect(spanToJSON(idleSpan).status).not.toEqual('deadline_exceeded'); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + // Wait some time + jest.advanceTimersByTime(TRACING_DEFAULTS.childSpanTimeout - 1000); + expect(spanToJSON(idleSpan).status).not.toEqual('deadline_exceeded'); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + // New span resets the timeout + startInactiveSpan({ name: 'span' }); + + jest.advanceTimersByTime(TRACING_DEFAULTS.childSpanTimeout - 1000); + expect(spanToJSON(idleSpan).status).not.toEqual('deadline_exceeded'); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + // New span resets the timeout + startInactiveSpan({ name: 'span' }); + + jest.advanceTimersByTime(TRACING_DEFAULTS.childSpanTimeout - 1000); + expect(spanToJSON(idleSpan).status).not.toEqual('deadline_exceeded'); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + // Wait for timeout to exceed + jest.advanceTimersByTime(1000); + expect(spanToJSON(idleSpan).status).not.toEqual('deadline_exceeded'); + expect(spanToJSON(idleSpan).timestamp).toBeDefined(); + }); + }); + + describe('disableAutoFinish', () => { + it('skips idle timeout if disableAutoFinish=true', () => { + const idleSpan = startIdleSpan({ name: 'idle span' }, { disableAutoFinish: true }); + expect(idleSpan).toBeDefined(); + + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + // Now emit a signal + getClient()!.emit('idleSpanEnableAutoFinish', idleSpan); + + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeDefined(); + }); + + it('skips span timeout if disableAutoFinish=true', () => { + const idleSpan = startIdleSpan({ name: 'idle span' }, { disableAutoFinish: true, finalTimeout: 99_999 }); + expect(idleSpan).toBeDefined(); + + startInactiveSpan({ name: 'inner' }); + + jest.advanceTimersByTime(TRACING_DEFAULTS.childSpanTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + jest.advanceTimersByTime(TRACING_DEFAULTS.childSpanTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + // Now emit a signal + getClient()!.emit('idleSpanEnableAutoFinish', idleSpan); + + jest.advanceTimersByTime(TRACING_DEFAULTS.childSpanTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeDefined(); + }); + + it('times out at final timeout if disableAutoFinish=true', () => { + const idleSpan = startIdleSpan({ name: 'idle span' }, { disableAutoFinish: true }); + expect(idleSpan).toBeDefined(); + + jest.advanceTimersByTime(TRACING_DEFAULTS.finalTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeDefined(); + }); + + it('ignores it if hook is emitted with other span', () => { + const span = startInactiveSpan({ name: 'other span' }); + const idleSpan = startIdleSpan({ name: 'idle span' }, { disableAutoFinish: true }); + expect(idleSpan).toBeDefined(); + + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + + // Now emit a signal, but with a different span + getClient()!.emit('idleSpanEnableAutoFinish', span); + + // This doesn't affect us! + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); + expect(spanToJSON(idleSpan).timestamp).toBeUndefined(); + }); + }); + + describe('trim end timestamp', () => { + it('trims end to highest child span end', () => { + const idleSpan = startIdleSpan({ name: 'foo', startTime: 1000 }); + expect(idleSpan).toBeDefined(); + + const span1 = startInactiveSpan({ name: 'span1', startTime: 1001 }); + span1?.end(1005); + + const span2 = startInactiveSpan({ name: 'span2', startTime: 1002 }); + span2?.end(1100); + + const span3 = startInactiveSpan({ name: 'span1', startTime: 1050 }); + span3?.end(1060); + + expect(getActiveSpan()).toBe(idleSpan); + + jest.runAllTimers(); + + expect(spanToJSON(idleSpan!).timestamp).toBe(1100); + }); + + it('keeps lower span endTime than highest child span end', () => { + const idleSpan = startIdleSpan({ name: 'foo', startTime: 1000 }); + expect(idleSpan).toBeDefined(); + + const span1 = startInactiveSpan({ name: 'span1', startTime: 999_999_999 }); + span1?.end(1005); + + const span2 = startInactiveSpan({ name: 'span2', startTime: 1002 }); + span2?.end(1100); + + const span3 = startInactiveSpan({ name: 'span1', startTime: 1050 }); + span3?.end(1060); + + expect(getActiveSpan()).toBe(idleSpan); + + jest.runAllTimers(); + + expect(spanToJSON(idleSpan!).timestamp).toBeLessThan(999_999_999); + expect(spanToJSON(idleSpan!).timestamp).toBeGreaterThan(1060); + }); + }); +}); diff --git a/packages/core/test/lib/tracing/idletransaction.test.ts b/packages/core/test/lib/tracing/idletransaction.test.ts deleted file mode 100644 index 56ed93abe5d9..000000000000 --- a/packages/core/test/lib/tracing/idletransaction.test.ts +++ /dev/null @@ -1,570 +0,0 @@ -/* eslint-disable deprecation/deprecation */ -import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; - -import { - IdleTransaction, - SentrySpan, - TRACING_DEFAULTS, - Transaction, - getClient, - getCurrentHub, - getCurrentScope, - getGlobalScope, - getIsolationScope, - setCurrentClient, - spanToJSON, - startInactiveSpan, - startSpan, - startSpanManual, -} from '../../../src'; -import { IdleTransactionSpanRecorder } from '../../../src/tracing/idletransaction'; - -const dsn = 'https://123@sentry.io/42'; -beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - getGlobalScope().clear(); - - const options = getDefaultTestClientOptions({ dsn, tracesSampleRate: 1 }); - const client = new TestClient(options); - setCurrentClient(client); - client.init(); -}); - -afterEach(() => { - jest.clearAllMocks(); -}); - -describe('IdleTransaction', () => { - describe('onScope', () => { - it('sets the transaction on the scope on creation if onScope is true', () => { - const transaction = new IdleTransaction( - { name: 'foo' }, - getCurrentHub(), - TRACING_DEFAULTS.idleTimeout, - TRACING_DEFAULTS.finalTimeout, - TRACING_DEFAULTS.heartbeatInterval, - true, - ); - transaction.initSpanRecorder(10); - - const scope = getCurrentScope(); - - // eslint-disable-next-line deprecation/deprecation - expect(scope.getTransaction()).toBe(transaction); - }); - - it('does not set the transaction on the scope on creation if onScope is falsey', () => { - const transaction = new IdleTransaction({ name: 'foo' }, getCurrentHub()); - transaction.initSpanRecorder(10); - - const scope = getCurrentScope(); - // eslint-disable-next-line deprecation/deprecation - expect(scope.getTransaction()).toBe(undefined); - }); - - it('removes sampled transaction from scope on finish if onScope is true', () => { - const transaction = new IdleTransaction( - { name: 'foo' }, - getCurrentHub(), - TRACING_DEFAULTS.idleTimeout, - TRACING_DEFAULTS.finalTimeout, - TRACING_DEFAULTS.heartbeatInterval, - true, - ); - transaction.initSpanRecorder(10); - - transaction.end(); - jest.runAllTimers(); - - const scope = getCurrentScope(); - // eslint-disable-next-line deprecation/deprecation - expect(scope.getTransaction()).toBe(undefined); - }); - - it('removes unsampled transaction from scope on finish if onScope is true', () => { - const transaction = new IdleTransaction( - { name: 'foo', sampled: false }, - getCurrentHub(), - TRACING_DEFAULTS.idleTimeout, - TRACING_DEFAULTS.finalTimeout, - TRACING_DEFAULTS.heartbeatInterval, - true, - ); - - transaction.end(); - jest.runAllTimers(); - - const scope = getCurrentScope(); - // eslint-disable-next-line deprecation/deprecation - expect(scope.getTransaction()).toBe(undefined); - }); - - it('does not remove transaction from scope on finish if another transaction was set there', () => { - const transaction = new IdleTransaction( - { name: 'foo' }, - getCurrentHub(), - TRACING_DEFAULTS.idleTimeout, - TRACING_DEFAULTS.finalTimeout, - TRACING_DEFAULTS.heartbeatInterval, - true, - ); - transaction.initSpanRecorder(10); - - const otherTransaction = new Transaction({ name: 'bar' }, getCurrentHub()); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(otherTransaction); - - transaction.end(); - jest.runAllTimers(); - - const scope = getCurrentScope(); - // eslint-disable-next-line deprecation/deprecation - expect(scope.getTransaction()).toBe(otherTransaction); - }); - }); - - beforeEach(() => { - jest.useFakeTimers(); - }); - - it('push and pops activities', () => { - const transaction = new IdleTransaction({ name: 'foo' }, getCurrentHub()); - const mockFinish = jest.spyOn(transaction, 'end'); - transaction.initSpanRecorder(10); - expect(transaction.activities).toMatchObject({}); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - const span = startInactiveSpan({ name: 'inner' })!; - expect(transaction.activities).toMatchObject({ [span.spanContext().spanId]: true }); - - expect(mockFinish).toHaveBeenCalledTimes(0); - - span.end(); - expect(transaction.activities).toMatchObject({}); - - jest.runOnlyPendingTimers(); - expect(mockFinish).toHaveBeenCalledTimes(1); - }); - - it('does not push activities if a span already has an end timestamp', () => { - const transaction = new IdleTransaction({ name: 'foo' }, getCurrentHub()); - transaction.initSpanRecorder(10); - expect(transaction.activities).toMatchObject({}); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - startInactiveSpan({ name: 'inner', startTimestamp: 1234, endTimestamp: 5678 }); - expect(transaction.activities).toMatchObject({}); - }); - - it('does not finish if there are still active activities', () => { - const transaction = new IdleTransaction({ name: 'foo' }, getCurrentHub()); - 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); - - expect(mockFinish).toHaveBeenCalledTimes(0); - expect(transaction.activities).toMatchObject({ [childSpan.spanContext().spanId]: true }); - }); - }); - - it('calls beforeFinish callback before finishing', () => { - const mockCallback1 = jest.fn(); - const mockCallback2 = jest.fn(); - const transaction = new IdleTransaction({ name: 'foo' }, getCurrentHub()); - 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); - - startSpan({ name: 'inner' }, () => {}); - - jest.runOnlyPendingTimers(); - expect(mockCallback1).toHaveBeenCalledTimes(1); - expect(mockCallback1).toHaveBeenLastCalledWith(transaction, expect.any(Number)); - expect(mockCallback2).toHaveBeenCalledTimes(1); - expect(mockCallback2).toHaveBeenLastCalledWith(transaction, expect.any(Number)); - }); - - it('filters spans on finish', () => { - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, getCurrentHub()); - transaction.initSpanRecorder(10); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - // regular child - should be kept - const regularSpan = startInactiveSpan({ - name: 'span1', - startTimestamp: spanToJSON(transaction).start_timestamp! + 2, - })!; - - // discardedSpan - startTimestamp is too large - startInactiveSpan({ name: 'span2', startTimestamp: 645345234 }); - - // Should be cancelled - will not finish - const cancelledSpan = startInactiveSpan({ - name: 'span3', - startTimestamp: spanToJSON(transaction).start_timestamp! + 4, - })!; - - regularSpan.end(spanToJSON(regularSpan).start_timestamp! + 4); - transaction.end(spanToJSON(transaction).start_timestamp! + 10); - - expect(transaction.spanRecorder).toBeDefined(); - if (transaction.spanRecorder) { - const spans = transaction.spanRecorder.spans; - expect(spans).toHaveLength(3); - expect(spans[0].spanContext().spanId).toBe(transaction.spanContext().spanId); - - // Regular SentrySpan - should not modified - expect(spans[1].spanContext().spanId).toBe(regularSpan.spanContext().spanId); - expect(spanToJSON(spans[1]).timestamp).not.toBe(spanToJSON(transaction).timestamp); - - // Cancelled SentrySpan - has endtimestamp of transaction - expect(spans[2].spanContext().spanId).toBe(cancelledSpan.spanContext().spanId); - expect(spanToJSON(spans[2]).status).toBe('cancelled'); - expect(spanToJSON(spans[2]).timestamp).toBe(spanToJSON(transaction).timestamp); - } - }); - - it('filters out spans that exceed final timeout', () => { - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, getCurrentHub(), 1000, 3000); - transaction.initSpanRecorder(10); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - const span = startInactiveSpan({ name: 'span', startTimestamp: spanToJSON(transaction).start_timestamp! + 2 })!; - span.end(spanToJSON(span).start_timestamp! + 10 + 30 + 1); - - transaction.end(spanToJSON(transaction).start_timestamp! + 50); - - expect(transaction.spanRecorder).toBeDefined(); - expect(transaction.spanRecorder!.spans).toHaveLength(1); - }); - - it('should record dropped transactions', async () => { - const transaction = new IdleTransaction( - { name: 'foo', startTimestamp: 1234, sampled: false }, - getCurrentHub(), - 1000, - ); - - const client = getClient()!; - - const recordDroppedEventSpy = jest.spyOn(client, 'recordDroppedEvent'); - - transaction.initSpanRecorder(10); - transaction.end(spanToJSON(transaction).start_timestamp! + 10); - - expect(recordDroppedEventSpy).toHaveBeenCalledWith('sample_rate', 'transaction'); - }); - - describe('_idleTimeout', () => { - it('finishes if no activities are added to the transaction', () => { - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, getCurrentHub()); - transaction.initSpanRecorder(10); - - jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); - expect(spanToJSON(transaction).timestamp).toBeDefined(); - }); - - it('does not finish if a activity is started', () => { - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, getCurrentHub()); - transaction.initSpanRecorder(10); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - startInactiveSpan({ name: 'span' }); - - jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); - expect(spanToJSON(transaction).timestamp).toBeUndefined(); - }); - - it('does not finish when idleTimeout is not exceed after last activity finished', () => { - const idleTimeout = 10; - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, getCurrentHub(), idleTimeout); - transaction.initSpanRecorder(10); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - startSpan({ name: 'span1' }, () => {}); - - jest.advanceTimersByTime(2); - - startSpan({ name: 'span2' }, () => {}); - - jest.advanceTimersByTime(8); - - expect(spanToJSON(transaction).timestamp).toBeUndefined(); - }); - - it('finish when idleTimeout is exceeded after last activity finished', () => { - const idleTimeout = 10; - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, getCurrentHub(), idleTimeout); - transaction.initSpanRecorder(10); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - startSpan({ name: 'span1' }, () => {}); - - jest.advanceTimersByTime(2); - - startSpan({ name: 'span2' }, () => {}); - - jest.advanceTimersByTime(10); - - expect(spanToJSON(transaction).timestamp).toBeDefined(); - }); - }); - - describe('cancelIdleTimeout', () => { - it('permanent idle timeout cancel is not restarted by child span start', () => { - const idleTimeout = 10; - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, getCurrentHub(), idleTimeout); - transaction.initSpanRecorder(10); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - const firstSpan = startInactiveSpan({ name: 'span1' })!; - transaction.cancelIdleTimeout(undefined, { restartOnChildSpanChange: false }); - const secondSpan = startInactiveSpan({ name: 'span2' })!; - firstSpan.end(); - secondSpan.end(); - - expect(spanToJSON(transaction).timestamp).toBeDefined(); - }); - - it('permanent idle timeout cancel finished the transaction with the last child', () => { - const idleTimeout = 10; - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, getCurrentHub(), idleTimeout); - transaction.initSpanRecorder(10); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - const firstSpan = startInactiveSpan({ name: 'span1' })!; - transaction.cancelIdleTimeout(undefined, { restartOnChildSpanChange: false }); - const secondSpan = startInactiveSpan({ name: 'span2' })!; - const thirdSpan = startInactiveSpan({ name: 'span3' })!; - - firstSpan.end(); - expect(spanToJSON(transaction).timestamp).toBeUndefined(); - - secondSpan.end(); - expect(spanToJSON(transaction).timestamp).toBeUndefined(); - - thirdSpan.end(); - expect(spanToJSON(transaction).timestamp).toBeDefined(); - }); - - it('permanent idle timeout cancel finishes transaction if there are no activities', () => { - const idleTimeout = 10; - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, getCurrentHub(), idleTimeout); - transaction.initSpanRecorder(10); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - startSpan({ name: 'span' }, () => {}); - - jest.advanceTimersByTime(2); - - transaction.cancelIdleTimeout(undefined, { restartOnChildSpanChange: false }); - - expect(spanToJSON(transaction).timestamp).toBeDefined(); - }); - - it('default idle cancel timeout is restarted by child span change', () => { - const idleTimeout = 10; - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, getCurrentHub(), idleTimeout); - transaction.initSpanRecorder(10); - // eslint-disable-next-line deprecation/deprecation - getCurrentScope().setSpan(transaction); - - startSpan({ name: 'span' }, () => {}); - - jest.advanceTimersByTime(2); - - transaction.cancelIdleTimeout(); - - startSpan({ name: 'span' }, () => {}); - - jest.advanceTimersByTime(8); - expect(spanToJSON(transaction).timestamp).toBeUndefined(); - - jest.advanceTimersByTime(2); - expect(spanToJSON(transaction).timestamp).toBeDefined(); - }); - }); - - describe('heartbeat', () => { - it('does not mark transaction as `DeadlineExceeded` if idle timeout has not been reached', () => { - // 20s to exceed 3 heartbeats - const transaction = new IdleTransaction({ name: 'foo' }, getCurrentHub(), 20000); - const mockFinish = jest.spyOn(transaction, 'end'); - - expect(spanToJSON(transaction).status).not.toEqual('deadline_exceeded'); - expect(mockFinish).toHaveBeenCalledTimes(0); - - // Beat 1 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(spanToJSON(transaction).status).not.toEqual('deadline_exceeded'); - expect(mockFinish).toHaveBeenCalledTimes(0); - - // Beat 2 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(spanToJSON(transaction).status).not.toEqual('deadline_exceeded'); - expect(mockFinish).toHaveBeenCalledTimes(0); - - // Beat 3 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(spanToJSON(transaction).status).not.toEqual('deadline_exceeded'); - expect(mockFinish).toHaveBeenCalledTimes(0); - }); - - it('finishes a transaction after 3 beats', () => { - const transaction = new IdleTransaction({ name: 'foo' }, getCurrentHub(), 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); - startInactiveSpan({ name: 'span' }); - - // Beat 1 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(0); - - // Beat 2 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(0); - - // Beat 3 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(1); - }); - - it('resets after new activities are added', () => { - const transaction = new IdleTransaction({ name: 'foo' }, getCurrentHub(), 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); - startInactiveSpan({ name: 'span' }); - - // Beat 1 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(0); - - const span = startInactiveSpan({ name: 'span' })!; // push activity - - // Beat 1 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(0); - - // Beat 2 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(0); - - startInactiveSpan({ name: 'span' }); // push activity - startInactiveSpan({ name: 'span' }); // push activity - - // Beat 1 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(0); - - // Beat 2 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(0); - - span.end(); // pop activity - - // Beat 1 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(0); - - // Beat 2 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(0); - - // Beat 3 - jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); - expect(mockFinish).toHaveBeenCalledTimes(1); - - // Heartbeat does not keep going after finish has been called - jest.runAllTimers(); - expect(mockFinish).toHaveBeenCalledTimes(1); - }); - }); -}); - -describe('IdleTransactionSpanRecorder', () => { - it('pushes and pops activities', () => { - const mockPushActivity = jest.fn(); - const mockPopActivity = jest.fn(); - const spanRecorder = new IdleTransactionSpanRecorder(mockPushActivity, mockPopActivity, '', 10); - expect(mockPushActivity).toHaveBeenCalledTimes(0); - expect(mockPopActivity).toHaveBeenCalledTimes(0); - - const span = new SentrySpan({ sampled: true }); - - expect(spanRecorder.spans).toHaveLength(0); - spanRecorder.add(span); - expect(spanRecorder.spans).toHaveLength(1); - - expect(mockPushActivity).toHaveBeenCalledTimes(1); - expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanContext().spanId); - expect(mockPopActivity).toHaveBeenCalledTimes(0); - - span.end(); - expect(mockPushActivity).toHaveBeenCalledTimes(1); - expect(mockPopActivity).toHaveBeenCalledTimes(1); - expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanContext().spanId); - }); - - it('does not push activities if a span has a timestamp', () => { - const mockPushActivity = jest.fn(); - const mockPopActivity = jest.fn(); - const spanRecorder = new IdleTransactionSpanRecorder(mockPushActivity, mockPopActivity, '', 10); - - const span = new SentrySpan({ sampled: true, startTimestamp: 765, endTimestamp: 345 }); - spanRecorder.add(span); - - expect(mockPushActivity).toHaveBeenCalledTimes(0); - }); - - it('does not push or pop transaction spans', () => { - const mockPushActivity = jest.fn(); - const mockPopActivity = jest.fn(); - - const transaction = new IdleTransaction({ name: 'foo' }, getCurrentHub()); - const spanRecorder = new IdleTransactionSpanRecorder( - mockPushActivity, - mockPopActivity, - transaction.spanContext().spanId, - 10, - ); - - spanRecorder.add(transaction); - expect(mockPushActivity).toHaveBeenCalledTimes(0); - expect(mockPopActivity).toHaveBeenCalledTimes(0); - }); -}); diff --git a/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts b/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts new file mode 100644 index 000000000000..d5643c3d3651 --- /dev/null +++ b/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts @@ -0,0 +1,37 @@ +import type { Span } from '@sentry/types'; +import { SPAN_STATUS_ERROR } from '../../../src/tracing'; +import { SentryNonRecordingSpan } from '../../../src/tracing/sentryNonRecordingSpan'; +import { TRACE_FLAG_NONE, spanIsSampled, spanToJSON } from '../../../src/utils/spanUtils'; + +describe('SentryNonRecordingSpan', () => { + it('satisfies the Span interface', () => { + const span: Span = new SentryNonRecordingSpan(); + + expect(span.spanContext()).toEqual({ + spanId: expect.any(String), + traceId: expect.any(String), + traceFlags: TRACE_FLAG_NONE, + }); + + expect(spanIsSampled(span)).toBe(false); + expect(span.isRecording()).toBe(false); + expect(spanToJSON(span)).toEqual({ + span_id: expect.any(String), + trace_id: expect.any(String), + }); + + // Ensure all methods work + span.end(); + span.end(123); + span.updateName('name'); + span.setAttribute('key', 'value'); + span.setAttributes({ key: 'value' }); + span.setStatus({ code: SPAN_STATUS_ERROR }); + + // but nothing is actually set/readable + expect(spanToJSON(span)).toEqual({ + span_id: expect.any(String), + trace_id: expect.any(String), + }); + }); +}); diff --git a/packages/core/test/lib/tracing/span.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts similarity index 99% rename from packages/core/test/lib/tracing/span.test.ts rename to packages/core/test/lib/tracing/sentrySpan.test.ts index 9edceb86c33d..e065aeff33aa 100644 --- a/packages/core/test/lib/tracing/span.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -1,9 +1,9 @@ import { timestampInSeconds } from '@sentry/utils'; -import { SentrySpan } from '../../../src'; +import { SentrySpan } from '../../../src/tracing/sentrySpan'; import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus'; import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, spanToJSON, spanToTraceContext } from '../../../src/utils/spanUtils'; -describe('span', () => { +describe('SentrySpan', () => { describe('name', () => { it('works with name', () => { const span = new SentrySpan({ name: 'span name' }); diff --git a/packages/core/test/lib/tracing/spanstatus.test.ts b/packages/core/test/lib/tracing/spanstatus.test.ts index 3c72406209ca..2ed356b1d73e 100644 --- a/packages/core/test/lib/tracing/spanstatus.test.ts +++ b/packages/core/test/lib/tracing/spanstatus.test.ts @@ -18,9 +18,9 @@ describe('setHttpStatus', () => { ])('applies the correct span status and http status code to the span (%s - $%s)', (code, status) => { const span = new SentrySpan({ name: 'test' }); - setHttpStatus(span!, code); + setHttpStatus(span, code); - const { status: spanStatus, data } = spanToJSON(span!); + const { status: spanStatus, data } = spanToJSON(span); expect(spanStatus).toBe(status); expect(data).toMatchObject({ 'http.response.status_code': code }); @@ -29,9 +29,9 @@ describe('setHttpStatus', () => { it("doesn't set the status for an unknown http status code", () => { const span = new SentrySpan({ name: 'test' }); - setHttpStatus(span!, 600); + setHttpStatus(span, 600); - const { status: spanStatus, data } = spanToJSON(span!); + const { status: spanStatus, data } = spanToJSON(span); expect(spanStatus).toBeUndefined(); expect(data).toMatchObject({ 'http.response.status_code': 600 }); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 37b72193d2be..b937b20338d1 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,25 +1,29 @@ -import type { Event, Span } from '@sentry/types'; +import type { Event, Span, StartSpanOptions } from '@sentry/types'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + Scope, addTracingExtensions, - getCurrentHub, getCurrentScope, getGlobalScope, getIsolationScope, + getMainCarrier, + setAsyncContextStrategy, setCurrentClient, - spanIsSampled, spanToJSON, withScope, } from '../../../src'; +import { getAsyncContextStrategy } from '../../../src/hub'; import { SentrySpan, continueTrace, - getActiveSpan, startInactiveSpan, startSpan, startSpanManual, + withActiveSpan, } from '../../../src/tracing'; -import { getSpanTree } from '../../../src/tracing/utils'; +import { SentryNonRecordingSpan } from '../../../src/tracing/sentryNonRecordingSpan'; +import { getActiveSpan, getRootSpan, getSpanDescendants } from '../../../src/utils/spanUtils'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; beforeAll(() => { @@ -41,7 +45,9 @@ describe('startSpan', () => { getIsolationScope().clear(); getGlobalScope().clear(); - const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); + setAsyncContextStrategy(undefined); + + const options = getDefaultTestClientOptions({ tracesSampleRate: 1 }); client = new TestClient(options); setCurrentClient(client); client.init(); @@ -76,27 +82,10 @@ describe('startSpan', () => { } }); - it('should return the same value as the callback if transactions are undefined', async () => { - // @ts-expect-error we are force overriding the transaction return to be undefined - // The `startTransaction` types are actually wrong - it can return undefined - // if tracingExtensions are not enabled - // eslint-disable-next-line deprecation/deprecation - jest.spyOn(getCurrentHub(), 'startTransaction').mockImplementationOnce(() => undefined); - - try { - const result = await startSpan({ name: 'GET users/[id]' }, () => { - return callback(); - }); - expect(result).toEqual(expected); - } catch (e) { - expect(e).toEqual(expected); - } - }); - it('creates a transaction', async () => { let _span: Span | undefined = undefined; - client.on('finishTransaction', transaction => { - _span = transaction; + client.on('spanEnd', span => { + _span = span; }); try { await startSpan({ name: 'GET users/[id]' }, () => { @@ -111,43 +100,14 @@ describe('startSpan', () => { expect(spanToJSON(_span!).status).toEqual(isError ? 'internal_error' : undefined); }); - it('allows traceparent information to be overriden', async () => { - let _span: Span | undefined = undefined; - client.on('finishTransaction', transaction => { - _span = transaction; - }); - try { - await startSpan( - { - name: 'GET users/[id]', - parentSampled: true, - traceId: '12345678901234567890123456789012', - parentSpanId: '1234567890123456', - }, - () => { - return callback(); - }, - ); - } catch (e) { - // - } - expect(_span).toBeDefined(); - - expect(spanIsSampled(_span!)).toEqual(true); - expect(spanToJSON(_span!).trace_id).toEqual('12345678901234567890123456789012'); - expect(spanToJSON(_span!).parent_span_id).toEqual('1234567890123456'); - }); - it('allows for transaction to be mutated', async () => { let _span: Span | undefined = undefined; - client.on('finishTransaction', transaction => { - _span = transaction; + client.on('spanEnd', span => { + _span = span; }); try { await startSpan({ name: 'GET users/[id]' }, span => { - if (span) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); - } + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); return callback(); }); } catch (e) { @@ -159,11 +119,13 @@ describe('startSpan', () => { it('creates a span with correct description', async () => { let _span: Span | undefined = undefined; - client.on('finishTransaction', transaction => { - _span = transaction; + client.on('spanEnd', span => { + if (span === getRootSpan(span)) { + _span = span; + } }); try { - await startSpan({ name: 'GET users/[id]', parentSampled: true }, () => { + await startSpan({ name: 'GET users/[id]' }, () => { return startSpan({ name: 'SELECT * from users' }, () => { return callback(); }); @@ -173,7 +135,7 @@ describe('startSpan', () => { } expect(_span).toBeDefined(); - const spans = getSpanTree(_span!); + const spans = getSpanDescendants(_span!); expect(spans).toHaveLength(2); expect(spanToJSON(spans[1]).description).toEqual('SELECT * from users'); @@ -183,11 +145,13 @@ describe('startSpan', () => { it('allows for span to be mutated', async () => { let _span: Span | undefined = undefined; - client.on('finishTransaction', transaction => { - _span = transaction; + client.on('spanEnd', span => { + if (span === getRootSpan(span)) { + _span = span; + } }); try { - await startSpan({ name: 'GET users/[id]', parentSampled: true }, () => { + await startSpan({ name: 'GET users/[id]' }, () => { return startSpan({ name: 'SELECT * from users' }, childSpan => { if (childSpan) { childSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query'); @@ -200,26 +164,27 @@ describe('startSpan', () => { } expect(_span).toBeDefined(); - const spans = getSpanTree(_span!); + const spans = getSpanDescendants(_span!); expect(spans).toHaveLength(2); expect(spanToJSON(spans[1]).op).toEqual('db.query'); }); - it.each([ - { origin: 'auto.http.browser' }, - { attributes: { 'sentry.origin': 'auto.http.browser' } }, - // attribute should take precedence over top level origin - { origin: 'manual', attributes: { 'sentry.origin': 'auto.http.browser' } }, - ])('correctly sets the span origin', async () => { + it('correctly sets the span origin', async () => { let _span: Span | undefined = undefined; - client.on('finishTransaction', transaction => { - _span = transaction; + client.on('spanEnd', span => { + _span = span; }); try { - await startSpan({ name: 'GET users/[id]', origin: 'auto.http.browser' }, () => { - return callback(); - }); + await startSpan( + { + name: 'GET users/[id]', + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser' }, + }, + () => { + return callback(); + }, + ); } catch (e) { // } @@ -229,7 +194,7 @@ describe('startSpan', () => { expect(jsonSpan).toEqual({ data: { 'sentry.origin': 'auto.http.browser', - 'sentry.sample_rate': 0, + 'sentry.sample_rate': 1, 'sentry.source': 'custom', }, origin: 'auto.http.browser', @@ -243,21 +208,35 @@ describe('startSpan', () => { }); }); + it('returns a non recording span if tracing is disabled', () => { + const options = getDefaultTestClientOptions({}); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + const span = startSpan({ name: 'GET users/[id]' }, span => { + return span; + }); + + expect(span).toBeDefined(); + expect(span).toBeInstanceOf(SentryNonRecordingSpan); + }); + it('creates & finishes span', async () => { - let _span: SentrySpan | undefined; - startSpan({ name: 'GET users/[id]' }, span => { + const span = startSpan({ name: 'GET users/[id]' }, span => { expect(span).toBeDefined(); - expect(spanToJSON(span!).timestamp).toBeUndefined(); - _span = span as SentrySpan; + expect(span).toBeInstanceOf(SentrySpan); + expect(spanToJSON(span).timestamp).toBeUndefined(); + return span; }); - expect(_span).toBeDefined(); - expect(spanToJSON(_span!).timestamp).toBeDefined(); + expect(span).toBeDefined(); + expect(spanToJSON(span).timestamp).toBeDefined(); }); it('allows to pass a `startTime`', () => { const start = startSpan({ name: 'outer', startTime: [1234, 0] }, span => { - return spanToJSON(span!).start_timestamp; + return spanToJSON(span).start_timestamp; }); expect(start).toEqual(1234); @@ -287,7 +266,7 @@ describe('startSpan', () => { expect(getCurrentScope()).not.toBe(initialScope); expect(getCurrentScope()).toBe(manualScope); expect(getActiveSpan()).toBe(span); - expect(spanToJSON(span!).parent_span_id).toBe('parent-span-id'); + expect(spanToJSON(span).parent_span_id).toBe('parent-span-id'); }); expect(getCurrentScope()).toBe(initialScope); @@ -407,18 +386,19 @@ describe('startSpan', () => { }); startSpan({ name: 'span' }, span => { - expect(span?.spanContext().traceId).toBe('99999999999999999999999999999999'); + expect(span.spanContext().traceId).toBe('99999999999999999999999999999999'); }); }); }); describe('onlyIfParent', () => { - it('does not create a span if there is no parent', () => { + it('starts a non recording span if there is no parent', () => { const span = startSpan({ name: 'test span', onlyIfParent: true }, span => { return span; }); - expect(span).toBeUndefined(); + expect(span).toBeDefined(); + expect(span).toBeInstanceOf(SentryNonRecordingSpan); }); it('creates a span if there is a parent', () => { @@ -431,6 +411,7 @@ describe('startSpan', () => { }); expect(span).toBeDefined(); + expect(span).toBeInstanceOf(SentrySpan); }); }); @@ -444,12 +425,9 @@ describe('startSpan', () => { setCurrentClient(client); client.init(); - startSpan( - { name: 'outer', attributes: { test1: 'aa', test2: 'aa' }, data: { test1: 'bb', test3: 'bb' } }, - outerSpan => { - expect(outerSpan).toBeDefined(); - }, - ); + startSpan({ name: 'outer', attributes: { test1: 'aa', test2: 'aa', test3: 'bb' } }, outerSpan => { + expect(outerSpan).toBeDefined(); + }); expect(tracesSampler).toBeCalledTimes(1); expect(tracesSampler).toHaveBeenLastCalledWith({ @@ -482,7 +460,7 @@ describe('startSpan', () => { startSpanManual({ name: 'my-span' }, span => { withScope(scope2 => { scope2.setTag('scope', 2); - span?.end(); + span.end(); }); }); }); @@ -509,22 +487,69 @@ describe('startSpan', () => { }); }); }); + + it('uses implementation from ACS, if it exists', () => { + const staticSpan = new SentrySpan({ spanId: 'aha' }); + + const carrier = getMainCarrier(); + + const customFn = jest.fn((_options: StartSpanOptions, callback: (span: Span) => string) => { + callback(staticSpan); + return 'aha'; + }) as typeof startSpan; + + const acs = { + ...getAsyncContextStrategy(carrier), + startSpan: customFn, + }; + setAsyncContextStrategy(acs); + + const result = startSpan({ name: 'GET users/[id]' }, span => { + expect(span).toEqual(staticSpan); + return 'oho?'; + }); + + expect(result).toBe('aha'); + }); }); describe('startSpanManual', () => { beforeEach(() => { + addTracingExtensions(); + + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + + setAsyncContextStrategy(undefined); + const options = getDefaultTestClientOptions({ tracesSampleRate: 1 }); client = new TestClient(options); setCurrentClient(client); client.init(); }); + it('returns a non recording span if tracing is disabled', () => { + const options = getDefaultTestClientOptions({}); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + const span = startSpanManual({ name: 'GET users/[id]' }, span => { + return span; + }); + + expect(span).toBeDefined(); + expect(span).toBeInstanceOf(SentryNonRecordingSpan); + }); + it('creates & finishes span', async () => { startSpanManual({ name: 'GET users/[id]' }, (span, finish) => { expect(span).toBeDefined(); - expect(spanToJSON(span!).timestamp).toBeUndefined(); + expect(span).toBeInstanceOf(SentrySpan); + expect(spanToJSON(span).timestamp).toBeUndefined(); finish(); - expect(spanToJSON(span!).timestamp).toBeDefined(); + expect(spanToJSON(span).timestamp).toBeDefined(); }); }); @@ -557,7 +582,7 @@ describe('startSpanManual', () => { expect(getCurrentScope()).not.toBe(initialScope); expect(getCurrentScope()).toBe(manualScope); expect(getActiveSpan()).toBe(span); - expect(spanToJSON(span!).parent_span_id).toBe('parent-span-id'); + expect(spanToJSON(span).parent_span_id).toBe('parent-span-id'); finish(); @@ -589,13 +614,13 @@ describe('startSpanManual', () => { startSpanManual({ name: 'inner transaction', forceTransaction: true }, span => { startSpanManual({ name: 'inner span 2' }, span => { // all good - span?.end(); + span.end(); }); - span?.end(); + span.end(); }); - span?.end(); + span.end(); }); - span?.end(); + span.end(); }); await client.flush(); @@ -677,8 +702,8 @@ describe('startSpanManual', () => { it('allows to pass a `startTime`', () => { const start = startSpanManual({ name: 'outer', startTime: [1234, 0] }, span => { - span?.end(); - return spanToJSON(span!).start_timestamp; + span.end(); + return spanToJSON(span).start_timestamp; }); expect(start).toEqual(1234); @@ -695,8 +720,8 @@ describe('startSpanManual', () => { }); startSpanManual({ name: 'span' }, span => { - expect(span?.spanContext().traceId).toBe('99999999999999999999999999999991'); - span?.end(); + expect(span.spanContext().traceId).toBe('99999999999999999999999999999991'); + span.end(); }); }); }); @@ -706,8 +731,8 @@ describe('startSpanManual', () => { const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => { return span; }); - - expect(span).toBeUndefined(); + expect(span).toBeDefined(); + expect(span).toBeInstanceOf(SentryNonRecordingSpan); }); it('creates a span if there is a parent', () => { @@ -720,6 +745,7 @@ describe('startSpanManual', () => { }); expect(span).toBeDefined(); + expect(span).toBeInstanceOf(SentrySpan); }); }); @@ -732,25 +758,70 @@ describe('startSpanManual', () => { }); }); }); + + it('uses implementation from ACS, if it exists', () => { + const staticSpan = new SentrySpan({ spanId: 'aha' }); + + const carrier = getMainCarrier(); + + const customFn = jest.fn((_options: StartSpanOptions, callback: (span: Span) => string) => { + callback(staticSpan); + return 'aha'; + }) as unknown as typeof startSpanManual; + + const acs = { + ...getAsyncContextStrategy(carrier), + startSpanManual: customFn, + }; + setAsyncContextStrategy(acs); + + const result = startSpanManual({ name: 'GET users/[id]' }, span => { + expect(span).toEqual(staticSpan); + return 'oho?'; + }); + + expect(result).toBe('aha'); + }); }); describe('startInactiveSpan', () => { beforeEach(() => { + addTracingExtensions(); + + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + + setAsyncContextStrategy(undefined); + const options = getDefaultTestClientOptions({ tracesSampleRate: 1 }); client = new TestClient(options); setCurrentClient(client); client.init(); }); + it('returns a non recording span if tracing is disabled', () => { + const options = getDefaultTestClientOptions({}); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + const span = startInactiveSpan({ name: 'GET users/[id]' }); + + expect(span).toBeDefined(); + expect(span).toBeInstanceOf(SentryNonRecordingSpan); + }); + it('creates & finishes span', async () => { const span = startInactiveSpan({ name: 'GET users/[id]' }); expect(span).toBeDefined(); - expect(spanToJSON(span!).timestamp).toBeUndefined(); + expect(span).toBeInstanceOf(SentrySpan); + expect(spanToJSON(span).timestamp).toBeUndefined(); - span?.end(); + span.end(); - expect(spanToJSON(span!).timestamp).toBeDefined(); + expect(spanToJSON(span).timestamp).toBeDefined(); }); it('does not set span on scope', () => { @@ -759,7 +830,7 @@ describe('startInactiveSpan', () => { expect(span).toBeDefined(); expect(getActiveSpan()).toBeUndefined(); - span?.end(); + span.end(); expect(getActiveSpan()).toBeUndefined(); }); @@ -775,10 +846,10 @@ describe('startInactiveSpan', () => { const span = startInactiveSpan({ name: 'GET users/[id]', scope: manualScope }); expect(span).toBeDefined(); - expect(spanToJSON(span!).parent_span_id).toBe('parent-span-id'); + expect(spanToJSON(span).parent_span_id).toBe('parent-span-id'); expect(getActiveSpan()).toBeUndefined(); - span?.end(); + span.end(); expect(getActiveSpan()).toBeUndefined(); }); @@ -884,7 +955,7 @@ describe('startInactiveSpan', () => { it('allows to pass a `startTime`', () => { const span = startInactiveSpan({ name: 'outer', startTime: [1234, 0] }); - expect(spanToJSON(span!).start_timestamp).toEqual(1234); + expect(spanToJSON(span).start_timestamp).toEqual(1234); }); it("picks up the trace id off the parent scope's propagation context", () => { @@ -898,8 +969,8 @@ describe('startInactiveSpan', () => { }); const span = startInactiveSpan({ name: 'span' }); - expect(span?.spanContext().traceId).toBe('99999999999999999999999999999991'); - span?.end(); + expect(span.spanContext().traceId).toBe('99999999999999999999999999999991'); + span.end(); }); }); @@ -907,17 +978,18 @@ describe('startInactiveSpan', () => { it('does not create a span if there is no parent', () => { const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); - expect(span).toBeUndefined(); + expect(span).toBeDefined(); + expect(span).toBeInstanceOf(SentryNonRecordingSpan); }); it('creates a span if there is a parent', () => { const span = startSpan({ name: 'parent span' }, () => { const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); - return span; }); expect(span).toBeDefined(); + expect(span).toBeInstanceOf(SentrySpan); }); }); @@ -934,7 +1006,7 @@ describe('startInactiveSpan', () => { setCurrentClient(client); client.init(); - let span: Span | undefined; + let span: Span; const scope = getCurrentScope(); scope.setTag('outer', 'foo'); @@ -947,7 +1019,7 @@ describe('startInactiveSpan', () => { withScope(scope => { scope.setTag('scope', 2); - span?.end(); + span.end(); }); await client.flush(); @@ -973,30 +1045,48 @@ describe('startInactiveSpan', () => { expect(childSpans).toContain(innerSpan); }); }); + + it('uses implementation from ACS, if it exists', () => { + const staticSpan = new SentrySpan({ spanId: 'aha' }); + + const carrier = getMainCarrier(); + + const customFn = jest.fn((_options: StartSpanOptions) => { + return staticSpan; + }) as unknown as typeof startInactiveSpan; + + const acs = { + ...getAsyncContextStrategy(carrier), + startInactiveSpan: customFn, + }; + setAsyncContextStrategy(acs); + + const result = startInactiveSpan({ name: 'GET users/[id]' }); + expect(result).toBe(staticSpan); + }); }); describe('continueTrace', () => { beforeEach(() => { - const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); + addTracingExtensions(); + + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + + setAsyncContextStrategy(undefined); + + const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); client = new TestClient(options); setCurrentClient(client); client.init(); }); it('works without trace & baggage data', () => { - const expectedContext = { - metadata: {}, - }; - - const result = continueTrace({ sentryTrace: undefined, baggage: undefined }, ctx => { - expect(ctx).toEqual(expectedContext); - return ctx; + const scope = continueTrace({ sentryTrace: undefined, baggage: undefined }, () => { + return getCurrentScope(); }); - expect(result).toEqual(expectedContext); - - const scope = getCurrentScope(); - expect(scope.getPropagationContext()).toEqual({ sampled: undefined, spanId: expect.any(String), @@ -1007,30 +1097,16 @@ describe('continueTrace', () => { }); it('works with trace data', () => { - const expectedContext = { - metadata: { - dynamicSamplingContext: {}, - }, - parentSampled: false, - parentSpanId: '1121201211212012', - traceId: '12312012123120121231201212312012', - }; - - const result = continueTrace( + const scope = continueTrace( { sentryTrace: '12312012123120121231201212312012-1121201211212012-0', baggage: undefined, }, - ctx => { - expect(ctx).toEqual(expectedContext); - return ctx; + () => { + return getCurrentScope(); }, ); - expect(result).toEqual(expectedContext); - - const scope = getCurrentScope(); - expect(scope.getPropagationContext()).toEqual({ dsc: {}, // DSC should be an empty object (frozen), because there was an incoming trace sampled: false, @@ -1043,33 +1119,16 @@ describe('continueTrace', () => { }); it('works with trace & baggage data', () => { - const expectedContext = { - metadata: { - dynamicSamplingContext: { - environment: 'production', - version: '1.0', - }, - }, - parentSampled: true, - parentSpanId: '1121201211212012', - traceId: '12312012123120121231201212312012', - }; - - const result = continueTrace( + const scope = continueTrace( { sentryTrace: '12312012123120121231201212312012-1121201211212012-1', baggage: 'sentry-version=1.0,sentry-environment=production', }, - ctx => { - expect(ctx).toEqual(expectedContext); - return ctx; + () => { + return getCurrentScope(); }, ); - expect(result).toEqual(expectedContext); - - const scope = getCurrentScope(); - expect(scope.getPropagationContext()).toEqual({ dsc: { environment: 'production', @@ -1085,33 +1144,16 @@ describe('continueTrace', () => { }); it('works with trace & 3rd party baggage data', () => { - const expectedContext = { - metadata: { - dynamicSamplingContext: { - environment: 'production', - version: '1.0', - }, - }, - parentSampled: true, - parentSpanId: '1121201211212012', - traceId: '12312012123120121231201212312012', - }; - - const result = continueTrace( + const scope = continueTrace( { sentryTrace: '12312012123120121231201212312012-1121201211212012-1', baggage: 'sentry-version=1.0,sentry-environment=production,dogs=great,cats=boring', }, - ctx => { - expect(ctx).toEqual(expectedContext); - return ctx; + () => { + return getCurrentScope(); }, ); - expect(result).toEqual(expectedContext); - - const scope = getCurrentScope(); - expect(scope.getPropagationContext()).toEqual({ dsc: { environment: 'production', @@ -1127,44 +1169,192 @@ describe('continueTrace', () => { }); it('returns response of callback', () => { - const expectedContext = { - metadata: { - dynamicSamplingContext: {}, - }, - parentSampled: false, - parentSpanId: '1121201211212012', - traceId: '12312012123120121231201212312012', - }; - const result = continueTrace( { sentryTrace: '12312012123120121231201212312012-1121201211212012-0', baggage: undefined, }, - ctx => { - return { ctx }; + () => { + return 'aha'; }, ); - expect(result).toEqual({ ctx: expectedContext }); + expect(result).toEqual('aha'); }); +}); - it('works without a callback', () => { - const expectedContext = { - metadata: { - dynamicSamplingContext: {}, - }, - parentSampled: false, - parentSpanId: '1121201211212012', - traceId: '12312012123120121231201212312012', - }; +describe('getActiveSpan', () => { + beforeEach(() => { + addTracingExtensions(); + + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + + setAsyncContextStrategy(undefined); + const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + }); + + it('works without an active span on the scope', () => { + const span = getActiveSpan(); + expect(span).toBeUndefined(); + }); + + it('works with an active span on the scope', () => { + const activeSpan = new SentrySpan({ spanId: 'aha' }); // eslint-disable-next-line deprecation/deprecation - const ctx = continueTrace({ - sentryTrace: '12312012123120121231201212312012-1121201211212012-0', - baggage: undefined, + getCurrentScope().setSpan(activeSpan); + + const span = getActiveSpan(); + expect(span).toBe(activeSpan); + }); + + it('uses implementation from ACS, if it exists', () => { + const staticSpan = new SentrySpan({ spanId: 'aha' }); + + const carrier = getMainCarrier(); + + const customFn = jest.fn(() => { + return staticSpan; + }) as typeof getActiveSpan; + + const acs = { + ...getAsyncContextStrategy(carrier), + getActiveSpan: customFn, + }; + setAsyncContextStrategy(acs); + + const result = getActiveSpan(); + expect(result).toBe(staticSpan); + }); +}); + +describe('withActiveSpan()', () => { + beforeAll(() => { + addTracingExtensions(); + }); + + beforeEach(() => { + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + + setAsyncContextStrategy(undefined); + + const options = getDefaultTestClientOptions({ enableTracing: true }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + }); + + it('should set the active span within the callback', () => { + expect.assertions(2); + const inactiveSpan = startInactiveSpan({ name: 'inactive-span' }); + + expect(getActiveSpan()).not.toBe(inactiveSpan); + + withActiveSpan(inactiveSpan, () => { + expect(getActiveSpan()).toBe(inactiveSpan); }); + }); + + it('should create child spans when calling startSpan within the callback', () => { + const inactiveSpan = startInactiveSpan({ name: 'inactive-span' }); + + const parentSpanId = withActiveSpan(inactiveSpan, () => { + return startSpan({ name: 'child-span' }, childSpan => { + return spanToJSON(childSpan).parent_span_id; + }); + }); + + expect(parentSpanId).toBe(inactiveSpan.spanContext().spanId); + }); + + it('when `null` is passed, no span should be active within the callback', () => { + expect.assertions(1); + startSpan({ name: 'parent-span' }, () => { + withActiveSpan(null, () => { + expect(getActiveSpan()).toBeUndefined(); + }); + }); + }); + + it('uses implementation from ACS, if it exists', () => { + const staticSpan = new SentrySpan({ spanId: 'aha' }); + const staticScope = new Scope(); + + const carrier = getMainCarrier(); + + const customFn = jest.fn((_span: Span | null, callback: (scope: Scope) => string) => { + callback(staticScope); + return 'aha'; + }) as typeof withActiveSpan; + + const acs = { + ...getAsyncContextStrategy(carrier), + withActiveSpan: customFn, + }; + setAsyncContextStrategy(acs); + + const result = withActiveSpan(staticSpan, scope => { + expect(scope).toBe(staticScope); + return 'oho'; + }); + expect(result).toBe('aha'); + }); +}); + +describe('span hooks', () => { + beforeEach(() => { + addTracingExtensions(); + + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + + const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('correctly emits span hooks', () => { + const startedSpans: string[] = []; + const endedSpans: string[] = []; + + client.on('spanStart', span => { + startedSpans.push(spanToJSON(span).description || ''); + }); + + client.on('spanEnd', span => { + endedSpans.push(spanToJSON(span).description || ''); + }); + + startSpan({ name: 'span1' }, () => { + startSpan({ name: 'span2' }, () => { + const span = startInactiveSpan({ name: 'span3' }); + + startSpanManual({ name: 'span5' }, span => { + startInactiveSpan({ name: 'span4' }); + span?.end(); + }); + + span?.end(); + }); + }); + + expect(startedSpans).toHaveLength(5); + expect(endedSpans).toHaveLength(4); - expect(ctx).toEqual(expectedContext); + expect(startedSpans).toEqual(['span1', 'span2', 'span3', 'span5', 'span4']); + expect(endedSpans).toEqual(['span5', 'span3', 'span2', 'span1']); }); }); diff --git a/packages/core/test/lib/transports/base.test.ts b/packages/core/test/lib/transports/base.test.ts index 1425c55c45a7..a8198ce38b3d 100644 --- a/packages/core/test/lib/transports/base.test.ts +++ b/packages/core/test/lib/transports/base.test.ts @@ -22,7 +22,7 @@ const ATTACHMENT_ENVELOPE = createEnvelope( length: 20, filename: 'test-file.txt', content_type: 'text/plain', - attachment_type: 'text', + attachment_type: 'event.attachment', }, 'attachment content', ] as AttachmentItem, diff --git a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts index 7543d9ed39e3..e6370931f4cf 100644 --- a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts +++ b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts @@ -1,5 +1,11 @@ -import type { Attachment, Breadcrumb, EventProcessor, ScopeData } from '@sentry/types'; -import { mergeAndOverwriteScopeData, mergeArray, mergeScopeData } from '../../../src/utils/applyScopeDataToEvent'; +import type { Attachment, Breadcrumb, Event, EventProcessor, EventType, ScopeData } from '@sentry/types'; +import { startInactiveSpan } from '../../../src'; +import { + applyScopeDataToEvent, + mergeAndOverwriteScopeData, + mergeArray, + mergeScopeData, +} from '../../../src/utils/applyScopeDataToEvent'; describe('mergeArray', () => { it.each([ @@ -158,3 +164,103 @@ describe('mergeScopeData', () => { }); }); }); + +describe('applyScopeDataToEvent', () => { + it("doesn't apply scope.transactionName to transaction events", () => { + const data: ScopeData = { + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + transactionName: 'foo', + }; + const event: Event = { type: 'transaction', transaction: '/users/:id' }; + + applyScopeDataToEvent(event, data); + + expect(event.transaction).toBe('/users/:id'); + }); + + it('applies the root span name to transaction events', () => { + const data: ScopeData = { + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + transactionName: 'foo', + span: { + attributes: {}, + startTime: 1, + endTime: 2, + status: 'ok', + name: 'bar', + // @ts-expect-error - we don't need to provide all span context fields + spanContext: () => ({}), + }, + }; + + const event: Event = { type: 'transaction' }; + + applyScopeDataToEvent(event, data); + + expect(event.transaction).toBe('bar'); + }); + + it("doesn't apply the root span name to non-transaction events", () => { + const data: ScopeData = { + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + transactionName: '/users/:id', + span: startInactiveSpan({ name: 'foo' }), + }; + const event: Event = { type: undefined }; + + applyScopeDataToEvent(event, data); + + expect(event.transaction).toBe('/users/:id'); + }); + + it.each([undefined, 'profile', 'replay_event', 'feedback'])( + 'applies scope.transactionName to event with type %s', + type => { + const data: ScopeData = { + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + transactionName: 'foo', + }; + const event: Event = { type: type as EventType, transaction: '/users/:id' }; + + applyScopeDataToEvent(event, data); + + expect(event.transaction).toBe('foo'); + }, + ); +}); diff --git a/packages/core/test/lib/utils/getRootSpan.ts b/packages/core/test/lib/utils/getRootSpan.ts deleted file mode 100644 index dcb33ac83e8c..000000000000 --- a/packages/core/test/lib/utils/getRootSpan.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SentrySpan, Transaction, getRootSpan } from '../../../src'; - -describe('getRootSpan', () => { - it('returns the root span of a span (SentrySpan)', () => { - const root = new SentrySpan({ 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 SentrySpan({ 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 56b07405b02c..b802c7bfb69f 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,6 +1,22 @@ +import type { Span, SpanAttributes, SpanStatus, SpanTimeInput } from '@sentry/types'; import { TRACEPARENT_REGEXP, timestampInSeconds } from '@sentry/utils'; -import { SPAN_STATUS_OK, SentrySpan, spanToTraceHeader } from '../../../src'; -import { spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_STATUS_ERROR, + SPAN_STATUS_OK, + SPAN_STATUS_UNSET, + SentrySpan, + Transaction, + addTracingExtensions, + setCurrentClient, + spanToTraceHeader, + startInactiveSpan, + startSpan, +} from '../../../src'; +import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils'; +import { getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils'; +import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; describe('spanToTraceHeader', () => { test('simple', () => { @@ -48,74 +64,148 @@ describe('spanTimeInputToSeconds', () => { }); describe('spanToJSON', () => { - it('works with a simple span', () => { - const span = new SentrySpan(); - expect(spanToJSON(span)).toEqual({ - span_id: span.spanContext().spanId, - trace_id: span.spanContext().traceId, - origin: 'manual', - start_timestamp: span['_startTime'], - data: { - 'sentry.origin': 'manual', - }, + describe('SentrySpan', () => { + it('works with a simple span', () => { + const span = new SentrySpan(); + expect(spanToJSON(span)).toEqual({ + span_id: span.spanContext().spanId, + trace_id: span.spanContext().traceId, + origin: 'manual', + start_timestamp: span['_startTime'], + data: { + 'sentry.origin': 'manual', + }, + }); }); - }); - it('works with a full span', () => { - const span = new SentrySpan({ - name: 'test name', - op: 'test op', - parentSpanId: '1234', - spanId: '5678', - traceId: 'abcd', - origin: 'auto', - startTimestamp: 123, - endTimestamp: 456, - }); - span.setStatus({ code: SPAN_STATUS_OK }); - - expect(spanToJSON(span)).toEqual({ - description: 'test name', - op: 'test op', - parent_span_id: '1234', - span_id: '5678', - status: 'ok', - trace_id: 'abcd', - origin: 'auto', - start_timestamp: 123, - timestamp: 456, - data: { - 'sentry.op': 'test op', - 'sentry.origin': 'auto', - }, + it('works with a full span', () => { + const span = new SentrySpan({ + name: 'test name', + op: 'test op', + parentSpanId: '1234', + spanId: '5678', + traceId: 'abcd', + startTimestamp: 123, + endTimestamp: 456, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + }, + }); + span.setStatus({ code: SPAN_STATUS_OK }); + + expect(spanToJSON(span)).toEqual({ + description: 'test name', + op: 'test op', + parent_span_id: '1234', + span_id: '5678', + status: 'ok', + trace_id: 'abcd', + origin: 'auto', + start_timestamp: 123, + timestamp: 456, + data: { + 'sentry.op': 'test op', + 'sentry.origin': 'auto', + }, + }); }); }); - 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 SentrySpan; - - expect(spanToJSON(span)).toEqual({ - span_id: 'span_id', - trace_id: 'trace_id', - origin: 'manual', - start_timestamp: 123, + describe('OpenTelemetry Span', () => { + function createMockedOtelSpan({ + spanId, + traceId, + attributes, + startTime, + name, + status, + endTime, + parentSpanId, + }: { + spanId: string; + traceId: string; + attributes: SpanAttributes; + startTime: SpanTimeInput; + name: string; + status: SpanStatus; + endTime: SpanTimeInput; + parentSpanId?: string; + }): Span { + return { + spanContext: () => { + return { + spanId, + traceId, + }; + }, + attributes, + startTime, + name, + status, + endTime, + parentSpanId, + } as OpenTelemetrySdkTraceBaseSpan; + } + + it('works with a simple span', () => { + const span = createMockedOtelSpan({ + spanId: 'SPAN-1', + traceId: 'TRACE-1', + name: 'test span', + startTime: 123, + endTime: [0, 0], + attributes: {}, + status: { code: SPAN_STATUS_UNSET }, + }); + + expect(spanToJSON(span)).toEqual({ + span_id: 'SPAN-1', + trace_id: 'TRACE-1', + start_timestamp: 123, + description: 'test span', + data: {}, + }); + }); + + it('works with a full span', () => { + const span = createMockedOtelSpan({ + spanId: 'SPAN-1', + traceId: 'TRACE-1', + name: 'test span', + startTime: 123, + endTime: 456, + attributes: { + attr1: 'value1', + attr2: 2, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + }, + status: { code: SPAN_STATUS_ERROR, message: 'unknown_error' }, + }); + + expect(spanToJSON(span)).toEqual({ + span_id: 'SPAN-1', + trace_id: 'TRACE-1', + start_timestamp: 123, + timestamp: 456, + description: 'test span', + op: 'test op', + origin: 'auto', + data: { + attr1: 'value1', + attr2: 2, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + }, + status: 'unknown_error', + }); }); }); - it('returns empty object if span does not have getter methods', () => { - // eslint-disable-next-line - const span = new SentrySpan().toJSON(); + it('returns empty object for unknown span implementation', () => { + const span = { other: 'other' }; - expect(spanToJSON(span as unknown as SentrySpan)).toEqual({}); + expect(spanToJSON(span as unknown as Span)).toEqual({}); }); }); @@ -130,3 +220,42 @@ describe('spanIsSampled', () => { expect(spanIsSampled(span)).toBe(false); }); }); + +describe('getRootSpan', () => { + beforeEach(() => { + const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); + setCurrentClient(client); + addTracingExtensions(); + }); + + it('returns the root span of a span that is a root span', () => { + const root = new SentrySpan({ name: 'test' }); + + expect(getRootSpan(root)).toBe(root); + }); + + it('returns the root span of a child span xxx', () => { + startSpan({ name: 'outer' }, root => { + startSpan({ name: 'inner' }, inner => { + expect(getRootSpan(inner)).toBe(root); + startSpan({ name: 'inner2' }, inner2 => { + expect(getRootSpan(inner2)).toBe(root); + + const inactiveSpan = startInactiveSpan({ name: 'inactived' }); + expect(getRootSpan(inactiveSpan)).toBe(root); + }); + }); + }); + }); + + it('returns the root span of a legacy transaction & its children', () => { + // eslint-disable-next-line deprecation/deprecation + const root = new Transaction({ name: 'test' }); + + expect(getRootSpan(root)).toBe(root); + + // eslint-disable-next-line deprecation/deprecation + const childSpan = root.startChild({ name: 'child' }); + expect(getRootSpan(childSpan)).toBe(root); + }); +}); diff --git a/packages/deno/.eslintrc.js b/packages/deno/.eslintrc.js index 0dabc2227a0d..0c539a61b1b2 100644 --- a/packages/deno/.eslintrc.js +++ b/packages/deno/.eslintrc.js @@ -4,7 +4,6 @@ module.exports = { rules: { '@sentry-internal/sdk/no-optional-chaining': 'off', '@sentry-internal/sdk/no-nullish-coalescing': 'off', - '@sentry-internal/sdk/no-unsupported-es6-methods': 'off', '@sentry-internal/sdk/no-class-field-initializers': 'off', }, }; diff --git a/packages/deno/README.md b/packages/deno/README.md index 67ac88fedc6e..502778cf8abb 100644 --- a/packages/deno/README.md +++ b/packages/deno/README.md @@ -16,16 +16,17 @@ - [Official SDK Docs](https://docs.sentry.io/quickstart/) - [TypeDoc](http://getsentry.github.io/sentry-javascript/) -The Sentry Deno SDK is in beta. Please help us improve the SDK by [reporting any issues or giving us feedback](https://github.com/getsentry/sentry-javascript/issues). +The Sentry Deno SDK is in beta. Please help us improve the SDK by +[reporting any issues or giving us feedback](https://github.com/getsentry/sentry-javascript/issues). ## Usage -To use this SDK, call `Sentry.init(options)` as early as possible in the main entry module. This will initialize the SDK and -hook into the environment. Note that you can turn off almost all side effects using the respective options. +To use this SDK, call `Sentry.init(options)` as early as possible in the main entry module. This will initialize the SDK +and hook into the environment. Note that you can turn off almost all side effects using the respective options. ```javascript // 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'; // or import from npm registry import * as Sentry from 'npm:@sentry/deno'; @@ -36,8 +37,8 @@ Sentry.init({ }); ``` -To set context information or send manual events, use the exported functions of the Deno SDK. Note that these -functions will not perform any action before you have called `init()`: +To set context information or send manual events, use the exported functions of the Deno SDK. Note that these functions +will not perform any action before you have called `init()`: ```javascript // Set user information, as well as tags and further extras @@ -61,6 +62,3 @@ Sentry.captureEvent({ ], }); ``` - - - diff --git a/packages/deno/package.json b/packages/deno/package.json index 6eebb79744ef..2fe693cc15cf 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -8,6 +8,15 @@ "license": "MIT", "module": "build/index.mjs", "types": "build/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/index.d.ts", + "default": "./build/index.mjs" + } + } + }, "publishConfig": { "access": "public" }, @@ -30,7 +39,7 @@ "deno-types": "node ./scripts/download-deno-types.mjs", "build": "run-s build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "yarn deno-types && rollup -c rollup.config.js", + "build:transpile": "yarn deno-types && rollup -c rollup.config.mjs", "build:types": "run-s deno-types build:types:tsc build:types:bundle", "build:types:tsc": "tsc -p tsconfig.types.json", "build:types:bundle": "rollup -c rollup.types.config.mjs", diff --git a/packages/deno/rollup.config.js b/packages/deno/rollup.config.mjs similarity index 100% rename from packages/deno/rollup.config.js rename to packages/deno/rollup.config.mjs diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index cc5f1afb4d8d..7f64e0e72132 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -21,8 +21,6 @@ export type { AddRequestDataToEventOptions } from '@sentry/utils'; export type { DenoOptions } from './types'; export { - // eslint-disable-next-line deprecation/deprecation - addGlobalEventProcessor, addEventProcessor, addBreadcrumb, captureException, @@ -32,22 +30,14 @@ export { createTransport, continueTrace, flush, - // eslint-disable-next-line deprecation/deprecation - getActiveTransaction, - // eslint-disable-next-line deprecation/deprecation - getCurrentHub, getClient, isInitialized, getCurrentScope, getGlobalScope, getIsolationScope, Hub, - // eslint-disable-next-line deprecation/deprecation - makeMain, setCurrentClient, Scope, - // eslint-disable-next-line deprecation/deprecation - startTransaction, SDK_VERSION, setContext, setExtra, @@ -63,6 +53,7 @@ export { withMonitor, setMeasurement, getActiveSpan, + getRootSpan, startSpan, startInactiveSpan, startSpanManual, @@ -99,10 +90,3 @@ export { normalizePathsIntegration } from './integrations/normalizepaths'; export { contextLinesIntegration } from './integrations/contextlines'; export { denoCronIntegration } from './integrations/deno-cron'; export { breadcrumbsIntegration } from './integrations/breadcrumbs'; - -import * as DenoIntegrations from './integrations'; - -/** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */ -export const Integrations = { - ...DenoIntegrations, -}; diff --git a/packages/deno/src/integrations/breadcrumbs.ts b/packages/deno/src/integrations/breadcrumbs.ts index 886d941d843f..58c75624b90d 100644 --- a/packages/deno/src/integrations/breadcrumbs.ts +++ b/packages/deno/src/integrations/breadcrumbs.ts @@ -17,6 +17,10 @@ interface BreadcrumbsOptions { const INTEGRATION_NAME = 'Breadcrumbs'; +/** + * Note: This `breadcrumbsIntegration` is almost the same as the one from @sentry/browser. + * The Deno-version does not support browser-specific APIs like dom, xhr and history. + */ const _breadcrumbsIntegration = ((options: Partial = {}) => { const _options = { console: true, @@ -42,8 +46,17 @@ const _breadcrumbsIntegration = ((options: Partial = {}) => }) satisfies IntegrationFn; /** - * This breadcrumbsIntegration is almost the same as the one from @sentry/browser. - * The Deno-version does not support browser-specific APIs like dom, xhr and history. + * Adds a breadcrumbs for console, fetch, and sentry events. + * + * Enabled by default in the Deno SDK. + * + * ```js + * Sentry.init({ + * integrations: [ + * Sentry.breadcrumbsIntegration(), + * ], + * }) + * ``` */ export const breadcrumbsIntegration = defineIntegration(_breadcrumbsIntegration); diff --git a/packages/deno/src/integrations/context.ts b/packages/deno/src/integrations/context.ts index ca0735c4e0ab..69ef164bb32d 100644 --- a/packages/deno/src/integrations/context.ts +++ b/packages/deno/src/integrations/context.ts @@ -1,5 +1,5 @@ -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; -import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Event, IntegrationFn } from '@sentry/types'; const INTEGRATION_NAME = 'DenoContext'; @@ -61,16 +61,17 @@ const _denoContextIntegration = (() => { }; }) satisfies IntegrationFn; -export const denoContextIntegration = defineIntegration(_denoContextIntegration); - /** - * Adds Deno context to events. - * @deprecated Use `denoContextintegration()` instead. + * Adds Deno related context to events. This includes contexts about app, device, os, v8, and TypeScript. + * + * Enabled by default in the Deno SDK. + * + * ```js + * Sentry.init({ + * integrations: [ + * Sentry.denoContextIntegration(), + * ], + * }) + * ``` */ -// eslint-disable-next-line deprecation/deprecation -export const DenoContext = convertIntegrationFnToClass(INTEGRATION_NAME, denoContextIntegration) as IntegrationClass< - Integration & { processEvent: (event: Event) => Promise } ->; - -// eslint-disable-next-line deprecation/deprecation -export type DenoContext = typeof DenoContext; +export const denoContextIntegration = defineIntegration(_denoContextIntegration); diff --git a/packages/deno/src/integrations/contextlines.ts b/packages/deno/src/integrations/contextlines.ts index 4b43c6bc34c2..3f3db47fb70a 100644 --- a/packages/deno/src/integrations/contextlines.ts +++ b/packages/deno/src/integrations/contextlines.ts @@ -1,5 +1,5 @@ -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; -import type { Event, Integration, IntegrationClass, IntegrationFn, StackFrame } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; import { LRUMap, addContextToFrame } from '@sentry/utils'; const INTEGRATION_NAME = 'ContextLines'; @@ -58,19 +58,20 @@ const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { }; }) satisfies IntegrationFn; -export const contextLinesIntegration = defineIntegration(_contextLinesIntegration); - /** - * Add node modules / packages to the event. - * @deprecated Use `contextLinesIntegration()` instead. + * Adds source context to event stacktraces. + * + * Enabled by default in the Deno SDK. + * + * ```js + * Sentry.init({ + * integrations: [ + * Sentry.contextLinesIntegration(), + * ], + * }) + * ``` */ -// eslint-disable-next-line deprecation/deprecation -export const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, contextLinesIntegration) as IntegrationClass< - Integration & { processEvent: (event: Event) => Promise } ->; - -// eslint-disable-next-line deprecation/deprecation -export type ContextLines = typeof ContextLines; +export const contextLinesIntegration = defineIntegration(_contextLinesIntegration); /** Processes an event and adds context lines */ async function addSourceContext(event: Event, contextLines: number): Promise { diff --git a/packages/deno/src/integrations/deno-cron.ts b/packages/deno/src/integrations/deno-cron.ts index 89030629864c..bdf330319c4c 100644 --- a/packages/deno/src/integrations/deno-cron.ts +++ b/packages/deno/src/integrations/deno-cron.ts @@ -1,5 +1,5 @@ -import { convertIntegrationFnToClass, defineIntegration, getClient, withMonitor } from '@sentry/core'; -import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { defineIntegration, getClient, withMonitor } from '@sentry/core'; +import type { Client, IntegrationFn } from '@sentry/types'; import { parseScheduleToString } from './deno-cron-format'; type CronOptions = { backoffSchedule?: number[]; signal?: AbortSignal }; @@ -60,16 +60,17 @@ const _denoCronIntegration = (() => { }; }) satisfies IntegrationFn; -export const denoCronIntegration = defineIntegration(_denoCronIntegration); - /** * Instruments Deno.cron to automatically capture cron check-ins. - * @deprecated Use `denoCronIntegration()` instead. + * + * Enabled by default in the Deno SDK. + * + * ```js + * Sentry.init({ + * integrations: [ + * Sentry.denoCronIntegration(), + * ], + * }) + * ``` */ -// eslint-disable-next-line deprecation/deprecation -export const DenoCron = convertIntegrationFnToClass(INTEGRATION_NAME, denoCronIntegration) as IntegrationClass< - Integration & { setup: (client: Client) => void } ->; - -// eslint-disable-next-line deprecation/deprecation -export type DenoCron = typeof DenoCron; +export const denoCronIntegration = defineIntegration(_denoCronIntegration); diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts index a653d7196246..8d9a2c1ac051 100644 --- a/packages/deno/src/integrations/globalhandlers.ts +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -1,18 +1,9 @@ import type { ServerRuntimeClient } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { convertIntegrationFnToClass } from '@sentry/core'; import { captureEvent } from '@sentry/core'; import { getClient } from '@sentry/core'; import { flush } from '@sentry/core'; -import type { - Client, - Event, - Integration, - IntegrationClass, - IntegrationFn, - Primitive, - StackParser, -} from '@sentry/types'; +import type { Client, Event, IntegrationFn, Primitive, StackParser } from '@sentry/types'; import { eventFromUnknownInput, isPrimitive } from '@sentry/utils'; type GlobalHandlersIntegrationsOptionKeys = 'error' | 'unhandledrejection'; @@ -42,20 +33,20 @@ const _globalHandlersIntegration = ((options?: GlobalHandlersIntegrations) => { }; }) satisfies IntegrationFn; -export const globalHandlersIntegration = defineIntegration(_globalHandlersIntegration); - /** - * Global handlers. - * @deprecated Use `globalHandlersIntegration()` instead. + * Instruments global `error` and `unhandledrejection` listeners in Deno. + * + * Enabled by default in the Deno SDK. + * + * ```js + * Sentry.init({ + * integrations: [ + * Sentry.globalHandlersIntegration(), + * ], + * }) + * ``` */ -// eslint-disable-next-line deprecation/deprecation -export const GlobalHandlers = convertIntegrationFnToClass( - INTEGRATION_NAME, - globalHandlersIntegration, -) as IntegrationClass void }>; - -// eslint-disable-next-line deprecation/deprecation -export type GlobalHandlers = typeof GlobalHandlers; +export const globalHandlersIntegration = defineIntegration(_globalHandlersIntegration); function installGlobalErrorHandler(client: Client): void { globalThis.addEventListener('error', data => { diff --git a/packages/deno/src/integrations/index.ts b/packages/deno/src/integrations/index.ts deleted file mode 100644 index 6870606066eb..000000000000 --- a/packages/deno/src/integrations/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint-disable deprecation/deprecation */ -export { DenoContext } from './context'; -export { GlobalHandlers } from './globalhandlers'; -export { NormalizePaths } from './normalizepaths'; -export { ContextLines } from './contextlines'; -export { DenoCron } from './deno-cron'; diff --git a/packages/deno/src/integrations/normalizepaths.ts b/packages/deno/src/integrations/normalizepaths.ts index d5304e9e62dd..1ea564a562ec 100644 --- a/packages/deno/src/integrations/normalizepaths.ts +++ b/packages/deno/src/integrations/normalizepaths.ts @@ -1,5 +1,5 @@ -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; -import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; import { createStackParser, dirname, nodeStackLineParser } from '@sentry/utils'; const INTEGRATION_NAME = 'NormalizePaths'; @@ -96,17 +96,17 @@ const _normalizePathsIntegration = (() => { }; }) satisfies IntegrationFn; -export const normalizePathsIntegration = defineIntegration(_normalizePathsIntegration); - /** * Normalises paths to the app root directory. - * @deprecated Use `normalizePathsIntegration()` instead. + * + * Enabled by default in the Deno SDK. + * + * ```js + * Sentry.init({ + * integrations: [ + * Sentry.normalizePathsIntegration(), + * ], + * }) + * ``` */ -// eslint-disable-next-line deprecation/deprecation -export const NormalizePaths = convertIntegrationFnToClass( - INTEGRATION_NAME, - normalizePathsIntegration, -) as IntegrationClass Event }>; - -// eslint-disable-next-line deprecation/deprecation -export type NormalizePaths = typeof NormalizePaths; +export const normalizePathsIntegration = defineIntegration(_normalizePathsIntegration); diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index 6cbbe0182902..937eae2893f8 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -12,6 +12,7 @@ snapshot[`captureException 1`] = ` }, os: { name: "{{platform}}", + version: "{{version}}", }, runtime: { name: "deno", @@ -47,7 +48,7 @@ snapshot[`captureException 1`] = ` filename: "app:///test/mod.test.ts", function: "?", in_app: true, - lineno: 47, + lineno: 46, post_context: [ "", " await delay(200);", @@ -73,7 +74,7 @@ snapshot[`captureException 1`] = ` filename: "app:///test/mod.test.ts", function: "something", in_app: true, - lineno: 44, + lineno: 43, post_context: [ " }", "", @@ -86,7 +87,7 @@ snapshot[`captureException 1`] = ` pre_context: [ "Deno.test('captureException', async t => {", " let ev: sentryTypes.Event | undefined;", - " const [, client] = getTestClient(event => {", + " const client = getTestClient(event => {", " ev = event;", " });", "", @@ -122,12 +123,21 @@ snapshot[`captureException 1`] = ` ], version: "{{version}}", }, + server_name: "{{server}}", timestamp: 0, } `; snapshot[`captureMessage 1`] = ` { + breadcrumbs: [ + { + category: "sentry.event", + event_id: "{{id}}", + message: "Error: Some unhandled error", + timestamp: 0, + }, + ], contexts: { app: { app_start_time: "{{time}}", @@ -138,6 +148,7 @@ snapshot[`captureMessage 1`] = ` }, os: { name: "{{platform}}", + version: "{{version}}", }, runtime: { name: "deno", @@ -182,6 +193,168 @@ snapshot[`captureMessage 1`] = ` ], version: "{{version}}", }, + server_name: "{{server}}", + timestamp: 0, +} +`; + +snapshot[`captureMessage twice 1`] = ` +{ + breadcrumbs: [ + { + category: "sentry.event", + event_id: "{{id}}", + message: "Error: Some unhandled error", + timestamp: 0, + }, + { + category: "sentry.event", + event_id: "{{id}}", + level: "info", + message: "Some error message", + timestamp: 0, + }, + ], + contexts: { + app: { + app_start_time: "{{time}}", + }, + device: { + arch: "{{arch}}", + processor_count: 0, + }, + os: { + name: "{{platform}}", + version: "{{version}}", + }, + runtime: { + name: "deno", + version: "{{version}}", + }, + trace: { + span_id: "{{id}}", + trace_id: "{{id}}", + }, + typescript: { + name: "TypeScript", + version: "{{version}}", + }, + v8: { + name: "v8", + version: "{{version}}", + }, + }, + environment: "production", + event_id: "{{id}}", + level: "info", + message: "Some error message", + platform: "javascript", + sdk: { + integrations: [ + "InboundFilters", + "FunctionToString", + "LinkedErrors", + "Dedupe", + "Breadcrumbs", + "DenoContext", + "ContextLines", + "NormalizePaths", + "GlobalHandlers", + ], + name: "sentry.javascript.deno", + packages: [ + { + name: "denoland:sentry", + version: "{{version}}", + }, + ], + version: "{{version}}", + }, + server_name: "{{server}}", + timestamp: 0, +} +`; + +snapshot[`captureMessage twice 2`] = ` +{ + breadcrumbs: [ + { + category: "sentry.event", + event_id: "{{id}}", + message: "Error: Some unhandled error", + timestamp: 0, + }, + { + category: "sentry.event", + event_id: "{{id}}", + level: "info", + message: "Some error message", + timestamp: 0, + }, + { + category: "sentry.event", + event_id: "{{id}}", + level: "info", + message: "Some error message", + timestamp: 0, + }, + ], + contexts: { + app: { + app_start_time: "{{time}}", + }, + device: { + arch: "{{arch}}", + processor_count: 0, + }, + os: { + name: "{{platform}}", + version: "{{version}}", + }, + runtime: { + name: "deno", + version: "{{version}}", + }, + trace: { + span_id: "{{id}}", + trace_id: "{{id}}", + }, + typescript: { + name: "TypeScript", + version: "{{version}}", + }, + v8: { + name: "v8", + version: "{{version}}", + }, + }, + environment: "production", + event_id: "{{id}}", + level: "info", + message: "Another error message", + platform: "javascript", + sdk: { + integrations: [ + "InboundFilters", + "FunctionToString", + "LinkedErrors", + "Dedupe", + "Breadcrumbs", + "DenoContext", + "ContextLines", + "NormalizePaths", + "GlobalHandlers", + ], + name: "sentry.javascript.deno", + packages: [ + { + name: "denoland:sentry", + version: "{{version}}", + }, + ], + version: "{{version}}", + }, + server_name: "{{server}}", timestamp: 0, } `; diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts index aae0963b8da5..656d9301b7ca 100644 --- a/packages/deno/test/mod.test.ts +++ b/packages/deno/test/mod.test.ts @@ -3,14 +3,14 @@ import { assertSnapshot } from 'https://deno.land/std@0.202.0/testing/snapshot.t import type { sentryTypes } from '../build-test/index.js'; import { sentryUtils } from '../build-test/index.js'; -import { DenoClient, Hub, Scope, getDefaultIntegrations } from '../build/index.mjs'; +import { DenoClient, getCurrentScope, getDefaultIntegrations } from '../build/index.mjs'; import { getNormalizedEvent } from './normalize.ts'; import { makeTestTransport } from './transport.ts'; function getTestClient( callback: (event?: sentryTypes.Event) => void, integrations: sentryTypes.Integration[] = [], -): [Hub, DenoClient] { +): DenoClient { const client = new DenoClient({ dsn: 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507', debug: true, @@ -21,11 +21,10 @@ function getTestClient( }), }); - const scope = new Scope(); - // eslint-disable-next-line deprecation/deprecation - const hub = new Hub(client, scope); + client.init(); + getCurrentScope().setClient(client); - return [hub, client]; + return client; } function delay(time: number): Promise { @@ -36,7 +35,7 @@ function delay(time: number): Promise { Deno.test('captureException', async t => { let ev: sentryTypes.Event | undefined; - const [, client] = getTestClient(event => { + const client = getTestClient(event => { ev = event; }); @@ -52,7 +51,7 @@ Deno.test('captureException', async t => { Deno.test('captureMessage', async t => { let ev: sentryTypes.Event | undefined; - const [, client] = getTestClient(event => { + const client = getTestClient(event => { ev = event; }); @@ -62,6 +61,23 @@ Deno.test('captureMessage', async t => { await assertSnapshot(t, ev); }); +Deno.test('captureMessage twice', async t => { + let ev: sentryTypes.Event | undefined; + const client = getTestClient(event => { + ev = event; + }); + + client.captureMessage('Some error message'); + + await delay(200); + await assertSnapshot(t, ev); + + client.captureMessage('Another error message'); + + await delay(200); + await assertSnapshot(t, ev); +}); + Deno.test('App runs without errors', async _ => { const cmd = new Deno.Command('deno', { args: ['run', '--allow-net=some-domain.com', './test/example.ts'], diff --git a/packages/deno/test/normalize.ts b/packages/deno/test/normalize.ts index 4dbbc8f3f6ec..9f4481e5fe4a 100644 --- a/packages/deno/test/normalize.ts +++ b/packages/deno/test/normalize.ts @@ -137,7 +137,7 @@ function normalizeEvent(event: sentryTypes.Event): sentryTypes.Event { event.contexts.os.name = '{{platform}}'; } - if (event.contexts?.os?.version) { + if (event.contexts?.os) { event.contexts.os.version = '{{version}}'; } @@ -197,8 +197,11 @@ function normalizeEvent(event: sentryTypes.Event): sentryTypes.Event { if (event.breadcrumbs) { for (const breadcrumb of event.breadcrumbs) { breadcrumb.timestamp = 0; + breadcrumb.event_id = '{{id}}'; } } + event.server_name = '{{server}}'; + return event; } diff --git a/packages/ember/README.md b/packages/ember/README.md index 0e56f6e30d47..2376869d107f 100644 --- a/packages/ember/README.md +++ b/packages/ember/README.md @@ -13,13 +13,12 @@ ## General -This package is an Ember addon that wraps `@sentry/browser`, with added functionality related to Ember. All methods available in -`@sentry/browser` can be imported from `@sentry/ember`. +This package is an Ember addon that wraps `@sentry/browser`, with added functionality related to Ember. All methods +available in `@sentry/browser` can be imported from `@sentry/ember`. ### Installation -As with other Ember addons, run: -`ember install @sentry/ember` +As with other Ember addons, run: `ember install @sentry/ember` Then add the following to your `/app.js` @@ -38,8 +37,9 @@ Then add the following to your `/app.js` ### Usage -To use this SDK, call `Sentry.init` before the application is initialized, in `app.js`. This will allow Sentry to capture information while your app is starting. -Any additional SDK settings can be modified via the usual config in `environment.js` for you, see the Additional Configuration section for more details. +To use this SDK, call `Sentry.init` before the application is initialized, in `app.js`. This will allow Sentry to +capture information while your app is starting. Any additional SDK settings can be modified via the usual config in +`environment.js` for you, see the Additional Configuration section for more details. ```javascript import Application from '@ember/application'; @@ -66,7 +66,8 @@ export default class App extends Application { ### Additional Configuration -Aside from configuration passed from this addon into `@sentry/browser` via the `sentry` property, there is also the following Ember specific configuration: +Aside from configuration passed from this addon into `@sentry/browser` via the `sentry` property, there is also the +following Ember specific configuration: ```javascript ENV['@sentry/ember'] = { @@ -93,8 +94,8 @@ ENV['@sentry/ember'] = { #### Disabling Performance - -`@sentry/ember` captures performance by default, if you would like to disable the automatic performance instrumentation, you can add the following to your `config/environment.js`: +`@sentry/ember` captures performance by default, if you would like to disable the automatic performance instrumentation, +you can add the following to your `config/environment.js`: ```javascript ENV['@sentry/ember'] = { @@ -102,7 +103,6 @@ ENV['@sentry/ember'] = { }; ``` - ### Performance #### Routes @@ -149,8 +149,8 @@ ENV['@sentry/ember'] = { }; ``` -Additionally, components whose render time is below a threshold (by default 2ms) will not be included as spans. -If you would like to change this threshold, add the following to your config: +Additionally, components whose render time is below a threshold (by default 2ms) will not be included as spans. If you +would like to change this threshold, add the following to your config: ```javascript ENV['@sentry/ember'] = { @@ -172,8 +172,8 @@ ENV['@sentry/ember'] = { ### Supported Versions -* **Ember.js**: v4.0 or above -* **Node**: v14.8 or above +- **Ember.js**: v4.0 or above +- **Node**: v14.18 or above ### Previous Integration @@ -182,8 +182,8 @@ this Ember addon to offer more Ember-specific error and performancing monitoring ## Testing -For this package itself, you can find example instrumentation in the `dummy` application, which is also used for testing. -To test with the dummy application, you must pass the dsn as an environment variable. +For this package itself, you can find example instrumentation in the `dummy` application, which is also used for +testing. To test with the dummy application, you must pass the dsn as an environment variable. ```javascript SENTRY_DSN=__DSN__ ember serve diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index 552fa1df7e78..b7b6d09ebeca 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -6,20 +6,21 @@ import type RouterService from '@ember/routing/router-service'; import { _backburner, run, scheduleOnce } from '@ember/runloop'; import type { EmberRunQueues } from '@ember/runloop/-private/types'; import { getOwnConfig, isTesting, macroCondition } from '@embroider/macros'; -import type { ExtendedBackburner } from '@sentry/ember/runloop'; -import type { Span } from '@sentry/types'; -import { GLOBAL_OBJ, browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/utils'; - -import type { BrowserClient } from '@sentry/browser'; +import type { + BrowserClient, + startBrowserTracingNavigationSpan as startBrowserTracingNavigationSpanType, + startBrowserTracingPageLoadSpan as startBrowserTracingPageLoadSpanType, +} from '@sentry/browser'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getClient, - startBrowserTracingNavigationSpan, - startBrowserTracingPageLoadSpan, startInactiveSpan, } from '@sentry/browser'; +import type { ExtendedBackburner } from '@sentry/ember/runloop'; +import type { Span } from '@sentry/types'; +import { GLOBAL_OBJ, browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/utils'; import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig } from '../types'; function getSentryConfig(): EmberSentryConfig { @@ -99,6 +100,8 @@ export function _instrumentEmberRouter( routerService: RouterService, routerMain: EmberRouterMain, config: EmberSentryConfig, + startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType, + startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType, ): void { const { disableRunloopPerformance } = config; const location = routerMain.location; @@ -119,9 +122,9 @@ export function _instrumentEmberRouter( const routeInfo = routerService.recognize(url); activeRootSpan = startBrowserTracingPageLoadSpan(client, { name: `route:${routeInfo.name}`, - origin: 'auto.pageload.ember', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.ember', url, toRoute: routeInfo.name, }, @@ -146,9 +149,9 @@ export function _instrumentEmberRouter( activeRootSpan = startBrowserTracingNavigationSpan(client, { name: `route:${toRoute}`, - origin: 'auto.navigation.ember', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.ember', fromRoute, toRoute, }, @@ -220,7 +223,7 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { }, name: 'runloop', op: `ui.ember.runloop.${queue}`, - startTimestamp: currentQueueStart, + startTime: currentQueueStart, })?.end(now); } currentQueueStart = undefined; @@ -292,8 +295,10 @@ function processComponentRenderAfter( startInactiveSpan({ name: payload.containerKey || payload.object, op, - origin: 'auto.ui.ember', - startTimestamp: begin.now, + startTime: begin.now, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + }, })?.end(now); } } @@ -369,8 +374,8 @@ function _instrumentInitialLoad(config: EmberSentryConfig): void { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const measure = measures[0]!; - const startTimestamp = (measure.startTime + browserPerformanceTimeOrigin) / 1000; - const endTimestamp = startTimestamp + measure.duration / 1000; + const startTime = (measure.startTime + browserPerformanceTimeOrigin) / 1000; + const endTime = startTime + measure.duration / 1000; startInactiveSpan({ op: 'ui.ember.init', @@ -378,8 +383,8 @@ function _instrumentInitialLoad(config: EmberSentryConfig): void { attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', }, - startTimestamp, - })?.end(endTimestamp); + startTime, + })?.end(endTime); performance.clearMarks(startName); performance.clearMarks(endName); @@ -411,7 +416,8 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance) // Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred. const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {}; - const { browserTracingIntegration } = await import('@sentry/browser'); + const { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } = + await import('@sentry/browser'); const idleTimeout = config.transitionTimeout || 5000; @@ -431,7 +437,7 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance) } // We _always_ call this, as it triggers the page load & navigation spans - _instrumentNavigation(appInstance, config); + _instrumentNavigation(appInstance, config, startBrowserTracingPageLoadSpan, startBrowserTracingNavigationSpan); // Skip instrumenting the stuff below again in tests, as these are not reset between tests if (isAlreadyInitialized) { @@ -443,7 +449,12 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance) _instrumentInitialLoad(config); } -function _instrumentNavigation(appInstance: ApplicationInstance, config: EmberSentryConfig): void { +function _instrumentNavigation( + appInstance: ApplicationInstance, + config: EmberSentryConfig, + startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType, + startBrowserTracingNavigationSpan: typeof startBrowserTracingNavigationSpanType, +): void { // eslint-disable-next-line ember/no-private-routing-service const routerMain = appInstance.lookup('router:main') as EmberRouterMain; let routerService = appInstance.lookup('service:router') as RouterService & { @@ -465,7 +476,13 @@ function _instrumentNavigation(appInstance: ApplicationInstance, config: EmberSe } routerService._hasMountedSentryPerformanceRouting = true; - _instrumentEmberRouter(routerService, routerMain, config); + _instrumentEmberRouter( + routerService, + routerMain, + config, + startBrowserTracingPageLoadSpan, + startBrowserTracingNavigationSpan, + ); } export default { diff --git a/packages/ember/package.json b/packages/ember/package.json index 4ebbdec64741..303888314735 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -83,7 +83,7 @@ "webpack": "~5.74.0" }, "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "ember": { "edition": "octane" diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index cf8a5bba796d..ca3c2f2235b9 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -12,7 +12,7 @@ "sentry" ], "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "files": [ "src" diff --git a/packages/eslint-config-sdk/src/base.js b/packages/eslint-config-sdk/src/base.js index 2b98cb6038dd..aa179260dc2c 100644 --- a/packages/eslint-config-sdk/src/base.js +++ b/packages/eslint-config-sdk/src/base.js @@ -151,9 +151,6 @@ module.exports = { }, ], - // Do not allow usage of functions we do not polyfill for ES5 - '@sentry-internal/sdk/no-unsupported-es6-methods': 'error', - // Do not allow usage of class field initializers '@sentry-internal/sdk/no-class-field-initializers': 'error', }, diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 5ddb37996613..f47cd6d4c44d 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -12,7 +12,7 @@ "sentry" ], "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "files": [ "src" diff --git a/packages/eslint-plugin-sdk/src/index.js b/packages/eslint-plugin-sdk/src/index.js index c03a042ca836..4390af285609 100644 --- a/packages/eslint-plugin-sdk/src/index.js +++ b/packages/eslint-plugin-sdk/src/index.js @@ -13,7 +13,6 @@ module.exports = { 'no-optional-chaining': require('./rules/no-optional-chaining'), 'no-nullish-coalescing': require('./rules/no-nullish-coalescing'), 'no-eq-empty': require('./rules/no-eq-empty'), - 'no-unsupported-es6-methods': require('./rules/no-unsupported-es6-methods'), 'no-class-field-initializers': require('./rules/no-class-field-initializers'), 'no-regexp-constructor': require('./rules/no-regexp-constructor'), }, diff --git a/packages/eslint-plugin-sdk/src/rules/no-unsupported-es6-methods.js b/packages/eslint-plugin-sdk/src/rules/no-unsupported-es6-methods.js deleted file mode 100644 index 85d32fb20e66..000000000000 --- a/packages/eslint-plugin-sdk/src/rules/no-unsupported-es6-methods.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -/** - * Taken and adapted from https://github.com/nkt/eslint-plugin-es5/blob/master/src/rules/no-es6-methods.js - */ - -module.exports = { - meta: { - docs: { - description: 'Forbid methods added in ES6 which are not polyfilled by Sentry.', - }, - schema: [], - }, - create(context) { - return { - CallExpression(node) { - if (!node.callee || !node.callee.property) { - return; - } - const functionName = node.callee.property.name; - - const es6ArrayFunctions = ['copyWithin', 'values', 'fill']; - const es6StringFunctions = ['repeat']; - - const es6Functions = [].concat(es6ArrayFunctions, es6StringFunctions); - if (es6Functions.indexOf(functionName) > -1) { - context.report({ - node: node.callee.property, - message: `ES6 methods not allowed: ${functionName}`, - }); - } - }, - }; - }, -}; diff --git a/packages/feedback/.eslintrc.js b/packages/feedback/.eslintrc.js index cf9985e769c0..0b547ffc828c 100644 --- a/packages/feedback/.eslintrc.js +++ b/packages/feedback/.eslintrc.js @@ -11,26 +11,6 @@ module.exports = { parserOptions: { project: ['tsconfig.test.json'], }, - rules: { - 'no-console': 'off', - }, - }, - { - files: ['test/**/*.ts'], - - rules: { - // most of these errors come from `new Promise(process.nextTick)` - '@typescript-eslint/unbound-method': 'off', - // TODO: decide if we want to enable this again after the migration - // We can take the freedom to be a bit more lenient with tests - '@typescript-eslint/no-floating-promises': 'off', - }, - }, - { - files: ['src/types/deprecated.ts'], - rules: { - '@typescript-eslint/naming-convention': 'off', - }, }, ], }; diff --git a/packages/feedback/README.md b/packages/feedback/README.md index fb5b20400a71..336e74da6593 100644 --- a/packages/feedback/README.md +++ b/packages/feedback/README.md @@ -6,15 +6,20 @@ # Sentry Integration for Feedback -This SDK is **considered experimental and in a beta state**. It may experience breaking changes, and may be discontinued at any time. Please reach out on -[GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have any feedback/concerns. +This SDK is **considered experimental and in a beta state**. It may experience breaking changes, and may be discontinued +at any time. Please reach out on [GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have +any feedback/concerns. -To view Feedback in Sentry, your [Sentry organization must be an early adopter](https://docs.sentry.io/product/accounts/early-adopter-features/). +To view Feedback in Sentry, your +[Sentry organization must be an early adopter](https://docs.sentry.io/product/accounts/early-adopter-features/). ## Installation -Please read the [offical integration documentation](https://docs.sentry.io/platforms/javascript/user-feedback/) for installation instructions. +Please read the [offical integration documentation](https://docs.sentry.io/platforms/javascript/user-feedback/) for +installation instructions. ## Configuration -The Feedback integration is highly customizable, please read the [official integration documentation](https://docs.sentry.io/platforms/javascript/user-feedback/configuration/) for the most up-to-date configuration options. +The Feedback integration is highly customizable, please read the +[official integration documentation](https://docs.sentry.io/platforms/javascript/user-feedback/configuration/) for the +most up-to-date configuration options. diff --git a/packages/feedback/package.json b/packages/feedback/package.json index d9364a8c8c7a..48d41f588447 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.8" + "node": ">=14.18" }, "files": [ "cjs", @@ -18,6 +18,19 @@ "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", "types": "build/npm/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/npm/types/index.d.ts", + "default": "./build/npm/esm/index.js" + }, + "require": { + "types": "./build/npm/types/index.d.ts", + "default": "./build/npm/cjs/index.js" + } + } + }, "typesVersions": { "<4.9": { "build/npm/types/index.d.ts": [ @@ -31,7 +44,8 @@ "dependencies": { "@sentry/core": "8.0.0-alpha.2", "@sentry/types": "8.0.0-alpha.2", - "@sentry/utils": "8.0.0-alpha.2" + "@sentry/utils": "8.0.0-alpha.2", + "preact": "^10.19.4" }, "scripts": { "build": "run-p build:transpile build:types build:bundle", @@ -40,7 +54,7 @@ "build:dev": "run-p build:transpile build:types", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", - "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8", + "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8 && yarn node ./scripts/shim-preact-export.js", "build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch", "build:dev:watch": "run-p build:transpile:watch build:types:watch", "build:transpile:watch": "yarn build:transpile --watch", diff --git a/packages/feedback/rollup.bundle.config.mjs b/packages/feedback/rollup.bundle.config.mjs index 3a9404947667..f5794d328409 100644 --- a/packages/feedback/rollup.bundle.config.mjs +++ b/packages/feedback/rollup.bundle.config.mjs @@ -1,13 +1,15 @@ import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; -const baseBundleConfig = makeBaseBundleConfig({ - bundleType: 'addon', - entrypoints: ['src/index.ts'], - jsVersion: 'es6', - licenseTitle: '@sentry-internal/feedback', - outputFileBase: () => 'bundles/feedback', -}); - -const builds = makeBundleConfigVariants(baseBundleConfig); - -export default builds; +export default makeBundleConfigVariants( + makeBaseBundleConfig({ + bundleType: 'addon', + entrypoints: ['src/index.ts'], + jsVersion: 'es6', + licenseTitle: '@sentry-internal/feedback', + outputFileBase: () => 'bundles/feedback', + sucrase: { + jsxPragma: 'h', + jsxFragmentPragma: 'Fragment', + }, + }), +); diff --git a/packages/feedback/rollup.npm.config.mjs b/packages/feedback/rollup.npm.config.mjs index 5a1800f23b08..3dc031c5ff82 100644 --- a/packages/feedback/rollup.npm.config.mjs +++ b/packages/feedback/rollup.npm.config.mjs @@ -12,5 +12,9 @@ export default makeNPMConfigVariants( preserveModules: false, }, }, + sucrase: { + jsxPragma: 'h', + jsxFragmentPragma: 'Fragment', + }, }), ); diff --git a/packages/feedback/scripts/shim-preact-export.js b/packages/feedback/scripts/shim-preact-export.js new file mode 100644 index 000000000000..bd74e4da0a05 --- /dev/null +++ b/packages/feedback/scripts/shim-preact-export.js @@ -0,0 +1,75 @@ +// preact does not support more modern TypeScript versions, which breaks our users that depend on older +// TypeScript versions. To fix this, we shim the types from preact to be any and remove the dependency on preact +// for types directly. This script is meant to be run after the build/npm/types-ts3.8 directory is created. + +// Path: build/npm/types-ts3.8/global.d.ts + +const fs = require('fs'); +const path = require('path'); + +/** + * This regex looks for preact imports we can replace and shim out. + * + * Example: + * import { ComponentChildren, VNode } from 'preact'; + */ +const preactImportRegex = /import\s*{\s*([\w\s,]+)\s*}\s*from\s*'preact'\s*;?/; + +function walk(dir) { + const files = fs.readdirSync(dir); + files.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.lstatSync(filePath); + if (stat.isDirectory()) { + walk(filePath); + } else { + if (filePath.endsWith('.d.ts')) { + const content = fs.readFileSync(filePath, 'utf8'); + const capture = preactImportRegex.exec(content); + if (capture) { + const groups = capture[1].split(',').map(s => s.trim()); + + // This generates a shim snippet to replace the type imports from preact + // It generates a snippet based on the capture groups of preactImportRegex. + // + // Example: + // + // import type { ComponentChildren, VNode } from 'preact'; + // becomes + // type ComponentChildren: any; + // type VNode: any; + const snippet = groups.reduce((acc, curr) => { + const searchableValue = curr.includes(' as ') ? curr.split(' as ')[1] : curr; + + // look to see if imported as value, then we have to use declare const + if (content.includes(`typeof ${searchableValue}`)) { + return `${acc}declare const ${searchableValue}: any;\n`; + } + + // look to see if generic type like Foo + if (content.includes(`${searchableValue}<`)) { + return `${acc}type ${searchableValue} = any;\n`; + } + + // otherwise we can just leave as type + return `${acc}type ${searchableValue} = any;\n`; + }, ''); + + // we then can remove the import from preact + const newContent = content.replace(preactImportRegex, '// replaced import from preact'); + + // and write the new content to the file + fs.writeFileSync(filePath, snippet + newContent, 'utf8'); + } + } + } + }); +} + +function run() { + // recurse through build/npm/types-ts3.8 directory + const dir = path.join('build', 'npm', 'types-ts3.8'); + walk(dir); +} + +run(); diff --git a/packages/feedback/src/constants/index.ts b/packages/feedback/src/constants/index.ts new file mode 100644 index 000000000000..9804fdedf431 --- /dev/null +++ b/packages/feedback/src/constants/index.ts @@ -0,0 +1,27 @@ +import { GLOBAL_OBJ } from '@sentry/utils'; + +export { DEFAULT_THEME } from './theme'; + +// exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser` +// prevents the browser package from being bundled in the CDN bundle, and avoids a +// circular dependency between the browser and feedback packages +export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; +export const DOCUMENT = WINDOW.document; +export const NAVIGATOR = WINDOW.navigator; + +export const ACTOR_LABEL = 'Report a Bug'; +export const CANCEL_BUTTON_LABEL = 'Cancel'; +export const SUBMIT_BUTTON_LABEL = 'Send Bug Report'; +export const FORM_TITLE = 'Report a Bug'; +export const EMAIL_PLACEHOLDER = 'your.email@example.org'; +export const EMAIL_LABEL = 'Email'; +export const MESSAGE_PLACEHOLDER = "What's the bug? What did you expect?"; +export const MESSAGE_LABEL = 'Description'; +export const NAME_PLACEHOLDER = 'Your Name'; +export const NAME_LABEL = 'Name'; +export const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!'; + +export const FEEDBACK_WIDGET_SOURCE = 'widget'; +export const FEEDBACK_API_SOURCE = 'api'; + +export const SUCCESS_MESSAGE_TIMEOUT = 5000; diff --git a/packages/feedback/src/constants.ts b/packages/feedback/src/constants/theme.ts similarity index 58% rename from packages/feedback/src/constants.ts rename to packages/feedback/src/constants/theme.ts index 07782968375f..7fff31f48964 100644 --- a/packages/feedback/src/constants.ts +++ b/packages/feedback/src/constants/theme.ts @@ -1,14 +1,8 @@ -import { GLOBAL_OBJ } from '@sentry/utils'; - -// exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser` -// prevents the browser package from being bundled in the CDN bundle, and avoids a -// circular dependency between the browser and feedback packages -export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; - const LIGHT_BACKGROUND = '#ffffff'; const INHERIT = 'inherit'; const SUBMIT_COLOR = 'rgba(108, 95, 199, 1)'; -const LIGHT_THEME = { + +export const LIGHT_THEME = { fontFamily: "system-ui, 'Helvetica Neue', Arial, sans-serif", fontSize: '14px', @@ -59,18 +53,3 @@ export const DEFAULT_THEME = { error: '#f55459', }, }; - -export const ACTOR_LABEL = 'Report a Bug'; -export const CANCEL_BUTTON_LABEL = 'Cancel'; -export const SUBMIT_BUTTON_LABEL = 'Send Bug Report'; -export const FORM_TITLE = 'Report a Bug'; -export const EMAIL_PLACEHOLDER = 'your.email@example.org'; -export const EMAIL_LABEL = 'Email'; -export const MESSAGE_PLACEHOLDER = "What's the bug? What did you expect?"; -export const MESSAGE_LABEL = 'Description'; -export const NAME_PLACEHOLDER = 'Your Name'; -export const NAME_LABEL = 'Name'; -export const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!'; - -export const FEEDBACK_WIDGET_SOURCE = 'widget'; -export const FEEDBACK_API_SOURCE = 'api'; diff --git a/packages/feedback/test/utils/TestClient.ts b/packages/feedback/src/core/TestClient.ts similarity index 84% rename from packages/feedback/test/utils/TestClient.ts rename to packages/feedback/src/core/TestClient.ts index 61156a3be8b0..39ed51fcff67 100644 --- a/packages/feedback/test/utils/TestClient.ts +++ b/packages/feedback/src/core/TestClient.ts @@ -4,34 +4,50 @@ import { resolvedSyncPromise } from '@sentry/utils'; export interface TestClientOptions extends ClientOptions, BrowserClientReplayOptions {} +/** + * + */ export class TestClient extends BaseClient { public constructor(options: TestClientOptions) { super(options); } + /** + * + */ public eventFromException(exception: any): PromiseLike { return resolvedSyncPromise({ exception: { values: [ { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access type: exception.name, + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access value: exception.message, - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ }, ], }, }); } + /** + * + */ public eventFromMessage(message: string, level: SeverityLevel = 'info'): PromiseLike { return resolvedSyncPromise({ message, level }); } } +/** + * + */ export function init(options: TestClientOptions): void { initAndBind(TestClient, options); } +/** + * + */ export function getDefaultClientOptions(options: Partial = {}): ClientOptions { return { integrations: [], diff --git a/packages/feedback/src/widget/Actor.css.ts b/packages/feedback/src/core/components/Actor.css.ts similarity index 74% rename from packages/feedback/src/widget/Actor.css.ts rename to packages/feedback/src/core/components/Actor.css.ts index 44bd60a3418e..4e7a9466cd1e 100644 --- a/packages/feedback/src/widget/Actor.css.ts +++ b/packages/feedback/src/core/components/Actor.css.ts @@ -1,16 +1,26 @@ +import { DOCUMENT } from '../../constants'; + /** * Creates