diff --git a/.craft.yml b/.craft.yml index 44d245311312..4a6534af681a 100644 --- a/.craft.yml +++ b/.craft.yml @@ -5,36 +5,30 @@ targets: # NPM Targets ## 1. Base Packages, node or browser SDKs depend on ## 1.1 Types - # TODO(v9): Remove - name: npm id: '@sentry/types' includeNames: /^sentry-types-\d.*\.tgz$/ - ## 1.2 Utils - # TODO(v9): Remove - - name: npm - id: '@sentry/utils' - includeNames: /^sentry-utils-\d.*\.tgz$/ - ## 1.3 Core SDK + ## 1.2 Core SDK - name: npm id: '@sentry/core' includeNames: /^sentry-core-\d.*\.tgz$/ - ## 1.4 Browser Utils package + ## 1.3 Browser Utils package - name: npm id: '@sentry-internal/browser-utils' includeNames: /^sentry-internal-browser-utils-\d.*\.tgz$/ - ## 1.5 Replay Internal package (browser only) + ## 1.4 Replay Internal package (browser only) - name: npm id: '@sentry-internal/replay' includeNames: /^sentry-internal-replay-\d.*\.tgz$/ - ## 1.6 OpenTelemetry package + ## 1.5 OpenTelemetry package - name: npm id: '@sentry/opentelemetry' includeNames: /^sentry-opentelemetry-\d.*\.tgz$/ - ## 1.7 Feedback package (browser only) + ## 1.6 Feedback package (browser only) - name: npm id: '@sentry-internal/feedback' includeNames: /^sentry-internal-feedback-\d.*\.tgz$/ - ## 1.8 ReplayCanvas package (browser only) + ## 1.7 ReplayCanvas package (browser only) - name: npm id: '@sentry-internal/replay-canvas' includeNames: /^sentry-internal-replay-canvas-\d.*\.tgz$/ @@ -83,14 +77,6 @@ targets: - name: npm id: '@sentry/deno' includeNames: /^sentry-deno-\d.*\.tgz$/ - - name: commit-on-git-repository - # This will publish on the Deno registry - id: getsentry/deno - archive: /^sentry-deno-\d.*\.tgz$/ - repositoryUrl: https://github.com/getsentry/sentry-deno.git - stripComponents: 1 - branch: main - createTag: true ## 5. Node-based Packages - name: npm @@ -142,37 +128,21 @@ targets: id: '@sentry-internal/eslint-config-sdk' includeNames: /^sentry-internal-eslint-config-sdk-\d.*\.tgz$/ - # TODO(v9): Remove this target - # NOTE: We publish the v8 layer under its own name so people on v8 can still get patches - # whenever we release a new v8 version—otherwise we would overwrite the current major lambda layer. - - name: aws-lambda-layer - includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha|rc)\.\d+)?\.zip$/ - layerName: SentryNodeServerlessSDKv8 - compatibleRuntimes: - - name: node - versions: - - nodejs10.x - - nodejs12.x - - nodejs14.x - - nodejs16.x - - nodejs18.x - - nodejs20.x - license: MIT - # AWS Lambda Layer target - - name: aws-lambda-layer - includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha|rc)\.\d+)?\.zip$/ - layerName: SentryNodeServerlessSDK - compatibleRuntimes: - - name: node - versions: - - nodejs10.x - - nodejs12.x - - nodejs14.x - - nodejs16.x - - nodejs18.x - - nodejs20.x - license: MIT + # TODO(v9): Once stable, re-add this target to publish the AWS Lambda layer + # - name: aws-lambda-layer + # includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha|rc)\.\d+)?\.zip$/ + # layerName: SentryNodeServerlessSDKv9 + # compatibleRuntimes: + # - name: node + # versions: + # - nodejs10.x + # - nodejs12.x + # - nodejs14.x + # - nodejs16.x + # - nodejs18.x + # - nodejs20.x + # license: MIT # CDN Bundle Target - name: gcs diff --git a/.github/ISSUE_TEMPLATE/flaky.yml b/.github/ISSUE_TEMPLATE/flaky.yml index a679cf98d328..2e86f8ebd869 100644 --- a/.github/ISSUE_TEMPLATE/flaky.yml +++ b/.github/ISSUE_TEMPLATE/flaky.yml @@ -19,7 +19,7 @@ body: id: job-name attributes: label: Name of Job - placeholder: "CI: Build & Test / Nextjs (Node 14) Tests" + placeholder: "CI: Build & Test / Nextjs (Node 18) Tests" description: name of job as reported in the status report validations: required: true diff --git a/.github/actions/restore-cache/action.yml b/.github/actions/restore-cache/action.yml index 6cd63a6550e4..e523cca6d904 100644 --- a/.github/actions/restore-cache/action.yml +++ b/.github/actions/restore-cache/action.yml @@ -5,9 +5,6 @@ inputs: dependency_cache_key: description: "The dependency cache key" required: true - node_version: - description: "If set, temporarily set node version to default one before installing, then revert to this version after." - required: false runs: using: "composite" @@ -24,19 +21,7 @@ runs: with: name: build-output - - name: Use default node version for install - if: inputs.node_version && steps.dep-cache.outputs.cache-hit != 'true' - uses: actions/setup-node@v4 - with: - node-version-file: 'package.json' - - name: Install dependencies if: steps.dep-cache.outputs.cache-hit != 'true' run: yarn install --ignore-engines --frozen-lockfile shell: bash - - - name: Revert node version to ${{ inputs.node_version }} - if: inputs.node_version && steps.dep-cache.outputs.cache-hit != 'true' - uses: actions/setup-node@v4 - with: - node-version: ${{ inputs.node_version }} diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml index 3becba39719e..1a8f76e430d1 100644 --- a/.github/dependency-review-config.yml +++ b/.github/dependency-review-config.yml @@ -7,3 +7,5 @@ allow-ghsas: - GHSA-fr5h-rqp8-mj6g # we need this for an E2E test for the minimum required version of Nuxt 3.7.0 - GHSA-v784-fjjh-f8r4 + # Next.js Cache poisoning - We require a vulnerable version for E2E testing + - GHSA-gp8f-8m3g-qvj9 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf9ba21376bb..24f3ee0454f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,8 @@ on: branches: - develop - master + - v9 + - v8 - release/** pull_request: merge_group: @@ -105,7 +107,7 @@ jobs: outputs: commit_label: '${{ env.COMMIT_SHA }}: ${{ env.COMMIT_MESSAGE }}' # Note: These next three have to be checked as strings ('true'/'false')! - is_develop: ${{ github.ref == 'refs/heads/develop' }} + is_base_branch: ${{ github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/v9' || github.ref == 'refs/heads/v8'}} is_release: ${{ startsWith(github.ref, 'refs/heads/release/') }} changed_profiling_node: ${{ steps.changed.outputs.profiling_node == 'true' }} changed_ci: ${{ steps.changed.outputs.workflow == 'true' }} @@ -126,7 +128,7 @@ jobs: timeout-minutes: 15 if: | needs.job_get_metadata.outputs.changed_any_code == 'true' || - needs.job_get_metadata.outputs.is_develop == 'true' || + needs.job_get_metadata.outputs.is_base_branch == 'true' || needs.job_get_metadata.outputs.is_release == 'true' || (needs.job_get_metadata.outputs.is_gitflow_sync == 'false' && needs.job_get_metadata.outputs.has_gitflow_label == 'false') steps: @@ -171,7 +173,7 @@ jobs: key: nx-Linux-${{ github.ref }}-${{ env.HEAD_COMMIT || github.sha }} # On develop branch, we want to _store_ the cache (so it can be used by other branches), but never _restore_ from it restore-keys: - ${{needs.job_get_metadata.outputs.is_develop == 'false' && env.NX_CACHE_RESTORE_KEYS || 'nx-never-restore'}} + ${{needs.job_get_metadata.outputs.is_base_branch == 'false' && env.NX_CACHE_RESTORE_KEYS || 'nx-never-restore'}} - name: Build packages # Set the CODECOV_TOKEN for Bundle Analysis @@ -219,7 +221,7 @@ jobs: timeout-minutes: 15 runs-on: ubuntu-20.04 if: - github.event_name == 'pull_request' || needs.job_get_metadata.outputs.is_develop == 'true' || + github.event_name == 'pull_request' || needs.job_get_metadata.outputs.is_base_branch == 'true' || needs.job_get_metadata.outputs.is_release == 'true' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) @@ -265,6 +267,8 @@ jobs: run: yarn lint:lerna - name: Lint C++ files run: yarn lint:clang + - name: Lint for ES compatibility + run: yarn lint:es-compatibility job_check_format: name: Check file formatting @@ -438,7 +442,7 @@ jobs: - name: Set up Deno uses: denoland/setup-deno@v2.0.1 with: - deno-version: v1.38.5 + deno-version: v2.1.5 - name: Restore caches uses: ./.github/actions/restore-cache with: @@ -457,7 +461,7 @@ jobs: strategy: fail-fast: false matrix: - node: [14, 16, 18, 20, 22] + node: [18, 20, 22] steps: - name: Check out base commit (${{ github.event.pull_request.base.sha }}) uses: actions/checkout@v4 @@ -476,7 +480,6 @@ jobs: uses: ./.github/actions/restore-cache with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - node_version: ${{ matrix.node == 14 && '14' || '' }} - name: Run affected tests run: yarn test:pr:node --base=${{ github.event.pull_request.base.sha }} @@ -713,7 +716,7 @@ jobs: strategy: fail-fast: false matrix: - node: [14, 16, 18, 20, 22] + node: [18, 20, 22] typescript: - false include: @@ -733,22 +736,18 @@ jobs: uses: ./.github/actions/restore-cache with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - node_version: ${{ matrix.node == 14 && '14' || '' }} - name: Overwrite typescript version - if: matrix.typescript - run: node ./scripts/use-ts-version.js ${{ matrix.typescript }} + if: matrix.typescript == '3.8' + run: node ./scripts/use-ts-3_8.js working-directory: dev-packages/node-integration-tests - name: Run integration tests - env: - NODE_VERSION: ${{ matrix.node }} - TS_VERSION: ${{ matrix.typescript }} working-directory: dev-packages/node-integration-tests run: yarn test job_remix_integration_tests: - name: Remix v${{ matrix.remix }} (Node ${{ matrix.node }}) Tests + name: Remix (Node ${{ matrix.node }}) Tests needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_remix == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04 @@ -757,11 +756,6 @@ jobs: fail-fast: false matrix: node: [18, 20, 22] - remix: [1, 2] - # Remix v2 only supports Node 18+, so run 16 tests separately - include: - - node: 16 - remix: 1 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 @@ -784,7 +778,6 @@ jobs: - name: Run integration tests env: NODE_VERSION: ${{ matrix.node }} - REMIX_VERSION: ${{ matrix.remix }} run: | cd packages/remix yarn test:integration:ci @@ -1207,7 +1200,8 @@ jobs: - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 10 - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:assert + run: | + xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:assert job_required_jobs_passed: name: All required jobs passed or were skipped @@ -1241,7 +1235,7 @@ jobs: echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1 job_compile_bindings_profiling_node: - name: Compile & Test Profiling Bindings (v${{ matrix.node }}) ${{ matrix.target_platform || matrix.os }}, ${{ matrix.node || matrix.container }}, ${{ matrix.arch || matrix.container }}, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }} + name: Compile profiling-node (v${{ matrix.node }}) ${{ matrix.target_platform || matrix.os }}, ${{ matrix.arch || matrix.container }}, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }} needs: [job_get_metadata, job_build] # Compiling bindings can be very slow (especially on windows), so only run precompile # Skip precompile unless we are on a release branch as precompile slows down CI times. @@ -1249,16 +1243,14 @@ jobs: (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || (needs.job_get_metadata.outputs.is_release == 'true') runs-on: ${{ matrix.os }} - container: ${{ matrix.container }} + container: + image: ${{ matrix.container }} timeout-minutes: 30 strategy: fail-fast: false matrix: include: # x64 glibc - - os: ubuntu-20.04 - node: 16 - binary: linux-x64-glibc-93 - os: ubuntu-20.04 node: 18 binary: linux-x64-glibc-108 @@ -1270,10 +1262,6 @@ jobs: binary: linux-x64-glibc-127 # x64 musl - - os: ubuntu-20.04 - container: node:16-alpine3.16 - binary: linux-x64-musl-93 - node: 16 - os: ubuntu-20.04 container: node:18-alpine3.17 node: 18 @@ -1288,10 +1276,6 @@ jobs: binary: linux-x64-musl-127 # arm64 glibc - - os: ubuntu-20.04 - arch: arm64 - node: 16 - binary: linux-arm64-glibc-93 - os: ubuntu-20.04 arch: arm64 node: 18 @@ -1306,11 +1290,6 @@ jobs: binary: linux-arm64-glibc-127 # arm64 musl - - os: ubuntu-20.04 - container: node:16-alpine3.16 - arch: arm64 - node: 16 - binary: linux-arm64-musl-93 - os: ubuntu-20.04 arch: arm64 container: node:18-alpine3.17 @@ -1328,10 +1307,6 @@ jobs: binary: linux-arm64-musl-127 # macos x64 - - os: macos-13 - node: 16 - arch: x64 - binary: darwin-x64-93 - os: macos-13 node: 18 arch: x64 @@ -1346,11 +1321,6 @@ jobs: binary: darwin-x64-127 # macos arm64 - - os: macos-13 - arch: arm64 - node: 16 - target_platform: darwin - binary: darwin-arm64-93 - os: macos-13 arch: arm64 node: 18 @@ -1368,10 +1338,6 @@ jobs: binary: darwin-arm64-127 # windows x64 - - os: windows-2022 - node: 16 - arch: x64 - binary: win32-x64-93 - os: windows-2022 node: 18 arch: x64 @@ -1397,8 +1363,13 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} + # Note: On alpine images, this does nothing + # The node version will be the one that is installed in the image + # If you want to change the node version, you need to change the image + # For non-alpine imgages, this will install the correct version of node - name: Setup Node uses: actions/setup-node@v4 + if: contains(matrix.container, 'alpine') == false with: node-version: ${{ matrix.node }} @@ -1415,10 +1386,10 @@ jobs: run: yarn config set network-timeout 600000 -g - name: Install dependencies - env: - SKIP_PLAYWRIGHT_BROWSER_INSTALL: "1" if: steps.restore-dependencies.outputs.cache-hit != 'true' run: yarn install --ignore-engines --frozen-lockfile + env: + SKIP_PLAYWRIGHT_BROWSER_INSTALL: "1" - name: Configure safe directory run: | @@ -1496,8 +1467,7 @@ jobs: BUILD_ARCH=arm64 \ yarn build:bindings:arm64 - - name: Build Monorepo - if: steps.restore-build.outputs.cache-hit != 'true' + - name: Build profiling-node & its dependencies run: yarn build --scope @sentry/profiling-node - name: Test Bindings diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index 8f63b6ca063b..776f8135178d 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -2,9 +2,18 @@ name: "CI: Enforce License Compliance" on: push: - branches: [master, develop, release/*] + branches: + - develop + - master + - v9 + - v8 + - release/** pull_request: - branches: [master, develop] + branches: + - develop + - master + - v9 + - v8 jobs: enforce-license-compliance: diff --git a/.size-limit.js b/.size-limit.js index 6e73c9234c09..c6e86836fd4c 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -15,7 +15,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init'), gzip: true, - limit: '24 KB', + limit: '24.1 KB', modifyWebpackConfig: function (config) { const webpack = require('webpack'); const TerserPlugin = require('terser-webpack-plugin'); @@ -93,7 +93,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'feedbackIntegration'), gzip: true, - limit: '41 KB', + limit: '42 KB', }, { name: '@sentry/browser (incl. sendFeedback)', diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6332f4c125..dcbfecab7da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,60 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @tjhiggins, @GrizliK1988, @davidturissini, @nwalters512, @aloisklink, @arturovt, @benjick, @maximepvrt, @mstrokin, and @kunal-511. Thank you for your contributions! + +- **feat(solidstart)!: Default to `--import` setup and add `autoInjectServerSentry` ([#14862](https://github.com/getsentry/sentry-javascript/pull/14862))** + +To enable the SolidStart SDK, wrap your SolidStart Config with `withSentry`. The `sentrySolidStartVite` plugin is now automatically +added by `withSentry` and you can pass the Sentry build-time options like this: + +```js +import { defineConfig } from '@solidjs/start/config'; +import { withSentry } from '@sentry/solidstart'; + +export default defineConfig( + withSentry( + { + /* Your SolidStart config options... */ + }, + { + // Options for setting up source maps + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + authToken: process.env.SENTRY_AUTH_TOKEN, + }, + ), +); +``` + +With the `withSentry` wrapper, the Sentry server config should not be added to the `public` directory anymore. +Add the Sentry server config in `src/instrument.server.ts`. Then, the server config will be placed inside the server build output as `instrument.server.mjs`. + +Now, there are two options to set up the SDK: + +1. **(recommended)** Provide an `--import` CLI flag to the start command like this (path depends on your server setup): + `node --import ./.output/server/instrument.server.mjs .output/server/index.mjs` +2. Add `autoInjectServerSentry: 'top-level-import'` and the Sentry config will be imported at the top of the server entry (comes with tracing limitations) + ```js + withSentry( + { + /* Your SolidStart config options... */ + }, + { + // Optional: Install Sentry with a top-level import + autoInjectServerSentry: 'top-level-import', + }, + ); + ``` + +## 9.0.0-alpha.0 + +This is an alpha release of the upcoming major release of version 9. +This release does not yet entail a comprehensive changelog as version 9 is not yet stable. + +For this release's iteration of the migration guide, see the [Migration Guide as per `9.0.0-alpha.0`](https://github.com/getsentry/sentry-javascript/blob/6e4b593adcc4ce951afa8ae0cda0605ecd226cda/docs/migration/v8-to-v9.md). +Please note that the migration guide is work in progress and subject to change. + ## 8.45.0 - feat(core): Add `handled` option to `captureConsoleIntegration` ([#14664](https://github.com/getsentry/sentry-javascript/pull/14664)) diff --git a/MIGRATION.md b/MIGRATION.md index 78657e360d2b..277bf2e1c44c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -5,6 +5,7 @@ These docs walk through how to migrate our JavaScript SDKs through different maj - Upgrading from [SDK 4.x to 5.x/6.x](./docs/migration/v4-to-v5_v6.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 [SDK 8.x to 9.x](./docs/migration/v8-to-v9.md) (Work in Progress - v9 is not released yet) # Upgrading from 7.x to 8.x @@ -869,53 +870,35 @@ or look at the TypeScript type definitions of `withSentryConfig`. #### Updated the recommended way of calling `Sentry.init()` -With version 8 of the SDK we will no longer support the use of `sentry.server.config.ts` and `sentry.edge.config.ts` -files. Instead, please initialize the Sentry Next.js SDK for the serverside in a -[Next.js instrumentation hook](https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation). -**`sentry.client.config.ts|js` is still supported and encouraged for initializing the clientside SDK.** +Version 8 of the Next.js SDK will require an additional `instrumentation.ts` file to execute the `sentry.server.config.js|ts` and `sentry.edge.config.js|ts` modules to initialize the SDK for the server-side. +The `instrumentation.ts` file is a Next.js native API called [instrumentation hook](https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation). -The following is an example of how to initialize the serverside SDK in a Next.js instrumentation hook: +To start using the Next.js instrumentation hook, follow these steps: -1. First, enable the Next.js instrumentation hook by setting the `experimental.instrumentationHook` to `true` in your - `next.config.js`. -2. Next, create a `instrumentation.ts|js` file in the root directory of your project (or in the `src` folder if you have - have one). -3. Now, export a `register` function from the `instrumentation.ts|js` file and call `Sentry.init()` inside of it: +1. First, enable the Next.js instrumentation hook by setting the [`experimental.instrumentationHook`](https://nextjs.org/docs/app/api-reference/next-config-js/instrumentationHook) to true in your `next.config.js`. (This step is no longer required with Next.js 15) - ```ts - import * as Sentry from '@sentry/nextjs'; - - export function register() { - if (process.env.NEXT_RUNTIME === 'nodejs') { - Sentry.init({ - dsn: 'YOUR_DSN', - // Your Node.js Sentry configuration... - }); - } - - if (process.env.NEXT_RUNTIME === 'edge') { - Sentry.init({ - dsn: 'YOUR_DSN', - // Your Edge Runtime Sentry configuration... - }); - } + ```JavaScript {filename:next.config.js} {2-4} + module.exports = { + experimental: { + instrumentationHook: true, // Not required on Next.js 15+ + }, } ``` - If you need to import a Node.js specific integration (like for example `@sentry/profiling-node`), you will have to - import the package using a dynamic import to prevent Next.js from bundling Node.js APIs into bundles for other - runtime environments (like the Browser or the Edge runtime). You can do so as follows: +2. Next, create a `instrumentation.ts|js` file in the root directory of your project (or in the src folder if you have have one). + +3. Now, export a register function from the `instrumentation.ts|js` file and import your `sentry.server.config.js|ts` and `sentry.edge.config.js|ts` modules: - ```ts + ```JavaScript {filename:instrumentation.js} import * as Sentry from '@sentry/nextjs'; export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { - const { nodeProfilingIntegration } = await import('@sentry/profiling-node'); - Sentry.init({ - dsn: 'YOUR_DSN', - integrations: [nodeProfilingIntegration()], - }); + await import('./sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('./sentry.edge.config'); } } ``` diff --git a/README.md b/README.md index db7000a9d51f..ed4fc189f11b 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,8 @@ package. Please refer to the README and instructions of those SDKs for more deta The current version of the SDK is 8.x. Version 7.x of the SDK will continue to receive critical bugfixes until end of 2024. +All SDKs require Node v18+ to run. ESM-only SDKs require Node v18.19.1+ to run. + ## Installation and Usage To install a SDK, simply add the high-level package, for example: diff --git a/biome.json b/biome.json index db56e24f80f0..010139fbaa82 100644 --- a/biome.json +++ b/biome.json @@ -35,17 +35,16 @@ } }, "ignore": [ - ".vscode/*", + ".vscode", "**/*.json", - ".next/**/*", - ".svelte-kit/**/*", "**/fixtures/*/*.json", "**/*.min.js", - ".next/**", - ".svelte-kit/**", - ".angular/**", + ".next", + ".nuxt", + ".svelte-kit", + ".angular", "angular.json", - "ember/instance-initializers/**", + "ember/instance-initializers", "ember/types.d.ts", "solidstart/*.d.ts", "solidstart/client/", diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/subject.js b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/subject.js index 405fb09bbac5..106ccaef33a8 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/subject.js +++ b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/subject.js @@ -2,7 +2,7 @@ const oldOnError = window.onerror; window.onerror = function () { console.log('custom error'); - oldOnError && oldOnError.apply(this, arguments); + oldOnError?.apply(this, arguments); }; window.doSomethingWrong(); diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 31a7ce76727e..58e378a0c582 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -4,12 +4,12 @@ "main": "index.js", "license": "MIT", "engines": { - "node": ">=14.18" + "node": ">=18" }, "private": true, "scripts": { "clean": "rimraf -g suites/**/dist loader-suites/**/dist tmp", - "install-browsers": "[[ -z \"$SKIP_PLAYWRIGHT_BROWSER_INSTALL\" ]] && yarn npx playwright install --with-deps || echo 'Skipping browser installation'", + "install-browsers": "[[ -z \"$SKIP_PLAYWRIGHT_BROWSER_INSTALL\" ]] && npx playwright install --with-deps || echo 'Skipping browser installation'", "lint": "eslint . --format stylish", "fix": "eslint . --format stylish --fix", "type-check": "tsc", @@ -46,14 +46,13 @@ "@sentry/browser": "8.45.0", "axios": "1.7.7", "babel-loader": "^8.2.2", + "fflate": "0.8.2", "html-webpack-plugin": "^5.5.0", - "pako": "^2.1.0", "webpack": "^5.95.0" }, "devDependencies": { "@types/glob": "8.0.0", - "@types/node": "^14.18.0", - "@types/pako": "^2.0.0", + "@types/node": "^18.19.1", "glob": "8.0.3" }, "volta": { diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js b/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js new file mode 100644 index 000000000000..d3f555f38933 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; +import { captureConsoleIntegration } from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [captureConsoleIntegration()], + attachStacktrace: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/subject.js b/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/subject.js new file mode 100644 index 000000000000..54f94f5ca4b3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/subject.js @@ -0,0 +1,8 @@ +console.log('console log'); +console.warn('console warn'); +console.error('console error'); +console.info('console info'); +console.trace('console trace'); + +console.error(new Error('console error with error object')); +console.trace(new Error('console trace with error object')); diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/test.ts b/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/test.ts new file mode 100644 index 000000000000..7e61bc43ce0e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/test.ts @@ -0,0 +1,176 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../utils/helpers'; + +sentryTest( + 'captures console messages correctly and adds a synthetic stack trace if `attachStackTrace` is set to `true`', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const [, events] = await Promise.all([page.goto(url), getMultipleSentryEnvelopeRequests(page, 7)]); + + expect(events).toHaveLength(7); + + const logEvent = events.find(event => event.message === 'console log'); + const warnEvent = events.find(event => event.message === 'console warn'); + const infoEvent = events.find(event => event.message === 'console info'); + const errorEvent = events.find(event => event.message === 'console error'); + const traceEvent = events.find(event => event.message === 'console trace'); + const errorWithErrorEvent = events.find( + event => event.exception?.values?.[0].value === 'console error with error object', + ); + const traceWithErrorEvent = events.find( + event => event.exception?.values?.[0].value === 'console trace with error object', + ); + + expect(logEvent).toEqual( + expect.objectContaining({ + level: 'log', + logger: 'console', + extra: { + arguments: ['console log'], + }, + message: 'console log', + }), + ); + expect(logEvent?.exception?.values![0]).toMatchObject({ + mechanism: { + handled: true, + type: 'console', + synthetic: true, + }, + value: 'console log', + stacktrace: { + frames: expect.any(Array), + }, + }); + + expect(warnEvent).toEqual( + expect.objectContaining({ + level: 'warning', + logger: 'console', + extra: { + arguments: ['console warn'], + }, + message: 'console warn', + }), + ); + expect(warnEvent?.exception?.values![0]).toMatchObject({ + mechanism: { + handled: true, + type: 'console', + synthetic: true, + }, + value: 'console warn', + stacktrace: { + frames: expect.any(Array), + }, + }); + + expect(infoEvent).toEqual( + expect.objectContaining({ + level: 'info', + logger: 'console', + extra: { + arguments: ['console info'], + }, + message: 'console info', + }), + ); + expect(infoEvent?.exception?.values![0]).toMatchObject({ + mechanism: { + handled: true, + type: 'console', + synthetic: true, + }, + value: 'console info', + stacktrace: { + frames: expect.any(Array), + }, + }); + + expect(errorEvent).toEqual( + expect.objectContaining({ + level: 'error', + logger: 'console', + extra: { + arguments: ['console error'], + }, + message: 'console error', + }), + ); + expect(errorEvent?.exception?.values![0]).toMatchObject({ + mechanism: { + handled: true, + type: 'console', + synthetic: true, + }, + value: 'console error', + stacktrace: { + frames: expect.any(Array), + }, + }); + + expect(traceEvent).toEqual( + expect.objectContaining({ + level: 'log', + logger: 'console', + extra: { + arguments: ['console trace'], + }, + message: 'console trace', + }), + ); + expect(traceEvent?.exception?.values![0]).toMatchObject({ + mechanism: { + handled: true, + type: 'console', + synthetic: true, + }, + value: 'console trace', + stacktrace: { + frames: expect.any(Array), + }, + }); + + expect(errorWithErrorEvent).toEqual( + expect.objectContaining({ + level: 'error', + logger: 'console', + extra: { + arguments: [ + { + message: 'console error with error object', + name: 'Error', + stack: expect.any(String), + }, + ], + }, + exception: expect.any(Object), + }), + ); + expect(errorWithErrorEvent?.exception?.values?.[0].value).toBe('console error with error object'); + expect(errorWithErrorEvent?.exception?.values?.[0].mechanism).toEqual({ + handled: true, + type: 'console', + }); + expect(traceWithErrorEvent).toEqual( + expect.objectContaining({ + level: 'log', + logger: 'console', + extra: { + arguments: [ + { + message: 'console trace with error object', + name: 'Error', + stack: expect.any(String), + }, + ], + }, + }), + ); + expect(traceWithErrorEvent?.exception?.values?.[0].value).toBe('console trace with error object'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js index 5d73c7da769c..1d611ebed805 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js @@ -6,5 +6,4 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [captureConsoleIntegration()], - autoSessionTracking: false, }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts index 6f8cfc20f4aa..5cef9bc7e949 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts @@ -17,10 +17,10 @@ sentryTest('it captures console messages correctly', async ({ getLocalTestUrl, p const errorEvent = events.find(event => event.message === 'console error'); const traceEvent = events.find(event => event.message === 'console trace'); const errorWithErrorEvent = events.find( - event => event.exception && event.exception.values?.[0].value === 'console error with error object', + event => event.exception?.values?.[0].value === 'console error with error object', ); const traceWithErrorEvent = events.find( - event => event.exception && event.exception.values?.[0].value === 'console trace with error object', + event => event.exception?.values?.[0].value === 'console trace with error object', ); expect(logEvent).toEqual( @@ -96,8 +96,7 @@ sentryTest('it captures console messages correctly', async ({ getLocalTestUrl, p ); expect(errorWithErrorEvent?.exception?.values?.[0].value).toBe('console error with error object'); expect(errorWithErrorEvent?.exception?.values?.[0].mechanism).toEqual({ - // TODO (v9): Adjust to true after changing the integration's default value - handled: false, + handled: true, type: 'console', }); expect(traceWithErrorEvent).toEqual( diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts index 97ecf2d961a7..5fedd521b2c2 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts @@ -22,8 +22,8 @@ sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true); - const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false); + const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === true); + const mainReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === false); await page.evaluate(() => { const Sentry = (window as any).Sentry; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js index aeea903b4eab..810539a8c07c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js @@ -13,7 +13,7 @@ Sentry.init({ // Also, no SDK has mock utils for FlagUsedHandler's. const MockLaunchDarkly = { initialize(_clientId, context, options) { - const flagUsedHandler = options && options.inspectors ? options.inspectors[0].method : undefined; + const flagUsedHandler = options.inspectors ? options.inspectors[0].method : undefined; return { variation(key, defaultValue) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts index 6046da6241be..b84c4c008e0e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts @@ -22,8 +22,8 @@ sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true); - const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false); + const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === true); + const mainReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === false); await page.evaluate(() => { const Sentry = (window as any).Sentry; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts index 7edb9b2e533b..60682b03e4a7 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts @@ -22,8 +22,8 @@ sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true); - const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false); + const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === true); + const mainReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === false); await page.evaluate(() => { const Sentry = (window as any).Sentry; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js new file mode 100644 index 000000000000..dc92fbc296a4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +window.UnleashClient = class { + isEnabled(x) { + return x; + } +}; + +window.Sentry = Sentry; +window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + integrations: [window.sentryUnleashIntegration], + debug: true, // Required to test logging. +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts new file mode 100644 index 000000000000..9b95d4d51c81 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts @@ -0,0 +1,59 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { shouldSkipFeatureFlagsTest } from '../../../../../utils/helpers'; + +sentryTest('Logs and returns if isEnabled does not match expected signature', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + const bundleKey = process.env.PW_BUNDLE || ''; + const hasDebug = !bundleKey.includes('_min'); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const errorLogs: string[] = []; + page.on('console', msg => { + if (msg.type() == 'error') { + errorLogs.push(msg.text()); + } + }); + + const results = await page.evaluate(() => { + const unleash = new (window as any).UnleashClient(); + const res1 = unleash.isEnabled('my-feature'); + const res2 = unleash.isEnabled(999); + const res3 = unleash.isEnabled({}); + return [res1, res2, res3]; + }); + + // Test that the expected results are still returned. Note isEnabled is identity function for this test. + expect(results).toEqual(['my-feature', 999, {}]); + + // Expected error logs. + if (hasDebug) { + expect(errorLogs).toEqual( + expect.arrayContaining([ + expect.stringContaining( + '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: my-feature (string), result: my-feature (string)', + ), + expect.stringContaining( + '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: 999 (number), result: 999 (number)', + ), + expect.stringContaining( + '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: [object Object] (object), result: [object Object] (object)', + ), + ]), + ); + } +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts new file mode 100644 index 000000000000..5bb72caddd24 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts @@ -0,0 +1,58 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils. + +sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + await page.evaluate(bufferSize => { + const client = new (window as any).UnleashClient(); + + client.isEnabled('feat1'); + client.isEnabled('strFeat'); + client.isEnabled('noPayloadFeat'); + client.isEnabled('jsonFeat'); + client.isEnabled('noVariantFeat'); + client.isEnabled('disabledFeat'); + + for (let i = 7; i <= bufferSize; i++) { + client.isEnabled(`feat${i}`); + } + client.isEnabled(`feat${bufferSize + 1}`); // eviction + client.isEnabled('noPayloadFeat'); // update (move to tail) + }, FLAG_BUFFER_SIZE); + + const reqPromise = waitForErrorRequest(page); + await page.locator('#error').click(); + const req = await reqPromise; + const event = envelopeRequestParser(req); + + const expectedFlags = [{ flag: 'strFeat', result: true }]; + expectedFlags.push({ flag: 'jsonFeat', result: true }); + expectedFlags.push({ flag: 'noVariantFeat', result: true }); + expectedFlags.push({ flag: 'disabledFeat', result: false }); + for (let i = 7; i <= FLAG_BUFFER_SIZE; i++) { + expectedFlags.push({ flag: `feat${i}`, result: false }); + } + expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: false }); + expectedFlags.push({ flag: 'noPayloadFeat', result: true }); + + expect(event.contexts?.flags?.values).toEqual(expectedFlags); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js new file mode 100644 index 000000000000..9f1f28730cf7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js @@ -0,0 +1,50 @@ +import * as Sentry from '@sentry/browser'; + +window.UnleashClient = class { + constructor() { + this._featureToVariant = { + strFeat: { name: 'variant1', enabled: true, feature_enabled: true, payload: { type: 'string', value: 'test' } }, + noPayloadFeat: { name: 'eu-west', enabled: true, feature_enabled: true }, + jsonFeat: { + name: 'paid-orgs', + enabled: true, + feature_enabled: true, + payload: { + type: 'json', + value: '{"foo": {"bar": "baz"}, "hello": [1, 2, 3]}', + }, + }, + + // Enabled feature with no configured variants. + noVariantFeat: { name: 'disabled', enabled: false, feature_enabled: true }, + + // Disabled feature. + disabledFeat: { name: 'disabled', enabled: false, feature_enabled: false }, + }; + + // Variant returned for features that don't exist. + // `feature_enabled` may be defined in prod, but we want to test the undefined case. + this._fallbackVariant = { + name: 'disabled', + enabled: false, + }; + } + + isEnabled(toggleName) { + const variant = this._featureToVariant[toggleName] || this._fallbackVariant; + return variant.feature_enabled || false; + } + + getVariant(toggleName) { + return this._featureToVariant[toggleName] || this._fallbackVariant; + } +}; + +window.Sentry = Sentry; +window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + integrations: [window.sentryUnleashIntegration], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js new file mode 100644 index 000000000000..e6697408128c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js @@ -0,0 +1,3 @@ +document.getElementById('error').addEventListener('click', () => { + throw new Error('Button triggered error'); +}); diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html similarity index 66% rename from dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html index 77906444cbce..9330c6c679f4 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html @@ -4,6 +4,6 @@ - Navigate + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts new file mode 100644 index 000000000000..2d821bf6c81d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts @@ -0,0 +1,65 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +import type { Scope } from '@sentry/browser'; + +sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true); + const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false); + + await page.evaluate(() => { + const Sentry = (window as any).Sentry; + const errorButton = document.querySelector('#error') as HTMLButtonElement; + const unleash = new (window as any).UnleashClient(); + + unleash.isEnabled('strFeat'); + + Sentry.withScope((scope: Scope) => { + unleash.isEnabled('disabledFeat'); + unleash.isEnabled('strFeat'); + scope.setTag('isForked', true); + if (errorButton) { + errorButton.click(); + } + }); + + unleash.isEnabled('noPayloadFeat'); + Sentry.getCurrentScope().setTag('isForked', false); + errorButton.click(); + return true; + }); + + const forkedReq = await forkedReqPromise; + const forkedEvent = envelopeRequestParser(forkedReq); + + const mainReq = await mainReqPromise; + const mainEvent = envelopeRequestParser(mainReq); + + expect(forkedEvent.contexts?.flags?.values).toEqual([ + { flag: 'disabledFeat', result: false }, + { flag: 'strFeat', result: true }, + ]); + + expect(mainEvent.contexts?.flags?.values).toEqual([ + { flag: 'strFeat', result: true }, + { flag: 'noPayloadFeat', result: true }, + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js index 7484c4a339e9..966bcff4925e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js @@ -1,8 +1,10 @@ import * as Sentry from '@sentry/browser'; +import { moduleMetadataIntegration } from '@sentry/browser'; + Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.moduleMetadataIntegration()], + integrations: [moduleMetadataIntegration()], beforeSend(event) { const moduleMetadataEntries = []; diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/test.ts b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/test.ts index 271314220f7a..986f379345d5 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/test.ts @@ -5,11 +5,6 @@ import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should provide module_metadata on stack frames in beforeSend', async ({ getLocalTestUrl, page }) => { - // moduleMetadataIntegration is not included in any CDN bundles - if (process.env.PW_BUNDLE?.startsWith('bundle')) { - sentryTest.skip(); - } - const url = await getLocalTestUrl({ testDir: __dirname }); const errorEvent = await getFirstSentryEnvelopeRequest(page, url); diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js index 885b1d2da2c1..5c5998839754 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js @@ -1,10 +1,13 @@ import * as Sentry from '@sentry/browser'; +import { moduleMetadataIntegration } from '@sentry/browser'; +import { rewriteFramesIntegration } from '@sentry/browser'; + Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ - Sentry.moduleMetadataIntegration(), - Sentry.rewriteFramesIntegration({ + moduleMetadataIntegration(), + rewriteFramesIntegration({ iteratee: frame => { return { ...frame, diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/test.ts b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/test.ts index bdabe0d63680..765bf7c21126 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/test.ts @@ -7,11 +7,6 @@ import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest( 'should provide module_metadata on stack frames in beforeSend even though an event processor (rewriteFramesIntegration) modified the filename', async ({ getLocalTestUrl, page }) => { - // moduleMetadataIntegration is not included in any CDN bundles - if (process.env.PW_BUNDLE?.startsWith('bundle')) { - sentryTest.skip(); - } - const url = await getLocalTestUrl({ testDir: __dirname }); const errorEvent = await getFirstSentryEnvelopeRequest(page, url); diff --git a/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/init.js b/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/init.js deleted file mode 100644 index 878444f52a0a..000000000000 --- a/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/init.js +++ /dev/null @@ -1,34 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', -}); - -Sentry.metrics.increment('increment'); -Sentry.metrics.increment('increment', 2); -Sentry.metrics.increment('increment', '3'); -Sentry.metrics.distribution('distribution', 42); -Sentry.metrics.distribution('distribution', '45'); -Sentry.metrics.gauge('gauge', 5); -Sentry.metrics.gauge('gauge', '15'); -Sentry.metrics.set('set', 'nope'); -Sentry.metrics.set('set', 'another'); - -Sentry.metrics.timing('timing', 99, 'hour'); -Sentry.metrics.timing('timingSync', () => { - sleepSync(200); -}); -Sentry.metrics.timing('timingAsync', async () => { - await new Promise(resolve => setTimeout(resolve, 200)); -}); - -function sleepSync(milliseconds) { - var start = new Date().getTime(); - for (var i = 0; i < 1e7; i++) { - if (new Date().getTime() - start > milliseconds) { - break; - } - } -} diff --git a/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/test.ts b/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/test.ts deleted file mode 100644 index 38b0139edad3..000000000000 --- a/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { expect } from '@playwright/test'; - -import { sentryTest } from '../../../utils/fixtures'; -import { - getFirstSentryEnvelopeRequest, - properEnvelopeRequestParser, - shouldSkipMetricsTest, -} from '../../../utils/helpers'; - -sentryTest('collects metrics', async ({ getLocalTestUrl, page }) => { - if (shouldSkipMetricsTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const statsdBuffer = await getFirstSentryEnvelopeRequest(page, url, properEnvelopeRequestParser); - const statsdString = new TextDecoder().decode(statsdBuffer); - // Replace all the Txxxxxx to remove the timestamps - const normalisedStatsdString = statsdString.replace(/T\d+\n?/g, 'T000000').trim(); - - const parts = normalisedStatsdString.split('T000000'); - - expect(parts).toEqual([ - 'increment@none:6|c|', - 'distribution@none:42:45|d|', - 'gauge@none:15:5:15:20:2|g|', - 'set@none:3387254:3443787523|s|', - 'timing@hour:99|d|', - expect.stringMatching(/timingSync@second:0.(\d+)\|d\|/), - expect.stringMatching(/timingAsync@second:0.(\d+)\|d\|/), - '', // trailing element - ]); -}); diff --git a/dev-packages/browser-integration-tests/suites/metrics/metricsShim/init.js b/dev-packages/browser-integration-tests/suites/metrics/metricsShim/init.js deleted file mode 100644 index 93c639cbdff9..000000000000 --- a/dev-packages/browser-integration-tests/suites/metrics/metricsShim/init.js +++ /dev/null @@ -1,13 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', -}); - -// This should not fail -Sentry.metrics.increment('increment'); -Sentry.metrics.distribution('distribution', 42); -Sentry.metrics.gauge('gauge', 5); -Sentry.metrics.set('set', 'nope'); diff --git a/dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts b/dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts deleted file mode 100644 index e8d0dbcfb274..000000000000 --- a/dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { expect } from '@playwright/test'; - -import { sentryTest } from '../../../utils/fixtures'; -import { shouldSkipMetricsTest } from '../../../utils/helpers'; - -sentryTest('exports shim metrics integration for non-tracing bundles', async ({ getLocalTestUrl, page }) => { - // Skip in tracing tests - if (!shouldSkipMetricsTest()) { - sentryTest.skip(); - } - - const consoleMessages: string[] = []; - page.on('console', msg => consoleMessages.push(msg.text())); - - let requestCount = 0; - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - requestCount++; - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); - - await page.goto(url); - - expect(requestCount).toBe(0); - expect(consoleMessages).toEqual([ - 'You are using metrics even though this bundle does not include tracing.', - 'You are using metrics even though this bundle does not include tracing.', - 'You are using metrics even though this bundle does not include tracing.', - 'You are using metrics even though this bundle does not include tracing.', - ]); -}); diff --git a/dev-packages/browser-integration-tests/suites/metrics/timing/init.js b/dev-packages/browser-integration-tests/suites/metrics/timing/init.js deleted file mode 100644 index 87f087b04ecf..000000000000 --- a/dev-packages/browser-integration-tests/suites/metrics/timing/init.js +++ /dev/null @@ -1,39 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - tracesSampleRate: 1.0, - release: '1.0.0', - autoSessionTracking: false, -}); - -window.timingSync = () => { - // Ensure we always have a wrapping span - return Sentry.startSpan({ name: 'manual span' }, () => { - return Sentry.metrics.timing('timingSync', () => { - sleepSync(200); - return 'sync done'; - }); - }); -}; - -window.timingAsync = () => { - // Ensure we always have a wrapping span - return Sentry.startSpan({ name: 'manual span' }, () => { - return Sentry.metrics.timing('timingAsync', async () => { - await new Promise(resolve => setTimeout(resolve, 200)); - return 'async done'; - }); - }); -}; - -function sleepSync(milliseconds) { - var start = new Date().getTime(); - for (var i = 0; i < 1e7; i++) { - if (new Date().getTime() - start > milliseconds) { - break; - } - } -} diff --git a/dev-packages/browser-integration-tests/suites/metrics/timing/test.ts b/dev-packages/browser-integration-tests/suites/metrics/timing/test.ts deleted file mode 100644 index 215f042dcdf7..000000000000 --- a/dev-packages/browser-integration-tests/suites/metrics/timing/test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { expect } from '@playwright/test'; - -import { sentryTest } from '../../../utils/fixtures'; -import { - envelopeRequestParser, - properEnvelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequest, -} from '../../../utils/helpers'; - -sentryTest('allows to wrap sync methods with a timing metric', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const beforeTime = Math.floor(Date.now() / 1000); - - const metricsPromiseReq = page.waitForRequest(req => { - const postData = req.postData(); - if (!postData) { - return false; - } - - try { - // this implies this is a metrics envelope - return typeof envelopeRequestParser(req) === 'string'; - } catch { - return false; - } - }); - - const transactionPromise = waitForTransactionRequest(page); - - await page.goto(url); - await page.waitForFunction('typeof window.timingSync === "function"'); - const response = await page.evaluate('window.timingSync()'); - - expect(response).toBe('sync done'); - - const statsdString = envelopeRequestParser(await metricsPromiseReq); - const transactionEvent = properEnvelopeRequestParser(await transactionPromise); - - expect(typeof statsdString).toEqual('string'); - - const parsedStatsd = /timingSync@second:(0\.\d+)\|d\|#(.+)\|T(\d+)/.exec(statsdString); - - expect(parsedStatsd).toBeTruthy(); - - const duration = parseFloat(parsedStatsd![1]); - const tags = parsedStatsd![2]; - const timestamp = parseInt(parsedStatsd![3], 10); - - expect(timestamp).toBeGreaterThanOrEqual(beforeTime); - expect(tags).toEqual('release:1.0.0,transaction:manual span'); - expect(duration).toBeGreaterThan(0.2); - expect(duration).toBeLessThan(1); - - expect(transactionEvent).toBeDefined(); - expect(transactionEvent.transaction).toEqual('manual span'); - - const spans = transactionEvent.spans || []; - - expect(spans.length).toBe(1); - const span = spans[0]; - expect(span.op).toEqual('metrics.timing'); - expect(span.description).toEqual('timingSync'); - expect(span.timestamp! - span.start_timestamp).toEqual(duration); - expect(span._metrics_summary).toEqual({ - 'd:timingSync@second': [ - { - count: 1, - max: duration, - min: duration, - sum: duration, - tags: { - release: '1.0.0', - transaction: 'manual span', - }, - }, - ], - }); -}); - -sentryTest('allows to wrap async methods with a timing metric', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const beforeTime = Math.floor(Date.now() / 1000); - - const metricsPromiseReq = page.waitForRequest(req => { - const postData = req.postData(); - if (!postData) { - return false; - } - - try { - // this implies this is a metrics envelope - return typeof envelopeRequestParser(req) === 'string'; - } catch { - return false; - } - }); - - const transactionPromise = waitForTransactionRequest(page); - - await page.goto(url); - await page.waitForFunction('typeof window.timingAsync === "function"'); - const response = await page.evaluate('window.timingAsync()'); - - expect(response).toBe('async done'); - - const statsdString = envelopeRequestParser(await metricsPromiseReq); - const transactionEvent = properEnvelopeRequestParser(await transactionPromise); - - expect(typeof statsdString).toEqual('string'); - - const parsedStatsd = /timingAsync@second:(0\.\d+)\|d\|#(.+)\|T(\d+)/.exec(statsdString); - - expect(parsedStatsd).toBeTruthy(); - - const duration = parseFloat(parsedStatsd![1]); - const tags = parsedStatsd![2]; - const timestamp = parseInt(parsedStatsd![3], 10); - - expect(timestamp).toBeGreaterThanOrEqual(beforeTime); - expect(tags).toEqual('release:1.0.0,transaction:manual span'); - expect(duration).toBeGreaterThan(0.2); - expect(duration).toBeLessThan(1); - - expect(transactionEvent).toBeDefined(); - expect(transactionEvent.transaction).toEqual('manual span'); - - const spans = transactionEvent.spans || []; - - expect(spans.length).toBe(1); - const span = spans[0]; - expect(span.op).toEqual('metrics.timing'); - expect(span.description).toEqual('timingAsync'); - expect(span.timestamp! - span.start_timestamp).toEqual(duration); - expect(span._metrics_summary).toEqual({ - 'd:timingAsync@second': [ - { - count: 1, - max: duration, - min: duration, - sum: duration, - tags: { - release: '1.0.0', - transaction: 'manual span', - }, - }, - ], - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/subject.js b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/subject.js index 6b195f6d2b20..273f740eea03 100644 --- a/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/subject.js +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/subject.js @@ -1,10 +1,10 @@ -const sentryCarrier = window && window.__SENTRY__; +const sentryCarrier = window?.__SENTRY__; /** * Simulate an old pre v8 SDK obtaining the hub from the global sentry carrier * and checking for the hub version. */ -const res = sentryCarrier.acs && sentryCarrier.acs.getCurrentScope(); +const res = sentryCarrier.acs?.getCurrentScope(); // Write back result into the document document.getElementById('currentScope').innerText = res && 'scope'; diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/subject.js b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/subject.js index 3de7e795e416..def990b268b5 100644 --- a/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/subject.js +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/subject.js @@ -1,10 +1,10 @@ -const sentryCarrier = window && window.__SENTRY__; +const sentryCarrier = window?.__SENTRY__; /** * Simulate an old pre v8 SDK obtaining the hub from the global sentry carrier * and checking for the hub version. */ -const res = sentryCarrier.hub && sentryCarrier.hub.isOlderThan(7); +const res = sentryCarrier.hub?.isOlderThan(7); // Write back result into the document document.getElementById('olderThan').innerText = res; diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/init.js rename to dev-packages/browser-integration-tests/suites/public-api/captureFeedback/init.js diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/simple_feedback/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/simple_feedback/subject.js similarity index 56% rename from dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/simple_feedback/subject.js rename to dev-packages/browser-integration-tests/suites/public-api/captureFeedback/simple_feedback/subject.js index 035199ab42f1..950b7701a043 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/simple_feedback/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/simple_feedback/subject.js @@ -1,6 +1,6 @@ -Sentry.captureUserFeedback({ +Sentry.captureFeedback({ eventId: 'test_event_id', email: 'test_email', - comments: 'test_comments', + message: 'test_comments', name: 'test_name', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/simple_feedback/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/simple_feedback/test.ts new file mode 100644 index 000000000000..acf97eba586f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/simple_feedback/test.ts @@ -0,0 +1,21 @@ +import { expect } from '@playwright/test'; +import type { FeedbackEvent } from '@sentry/core'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should capture simple user feedback', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.contexts).toMatchObject( + expect.objectContaining({ + feedback: { + contact_email: 'test_email', + message: 'test_comments', + name: 'test_name', + }, + }), + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js similarity index 61% rename from dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/init.js rename to dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js index 866263273351..099425cd3f2f 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js @@ -5,11 +5,11 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', beforeSend(event) { - Sentry.captureUserFeedback({ - event_id: event.event_id, + Sentry.captureFeedback({ + associatedEventId: event.event_id, name: 'John Doe', email: 'john@doe.com', - comments: 'This feedback should be attached associated with the captured error', + message: 'This feedback should be attached associated with the captured error', }); return event; }, diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/subject.js rename to dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/subject.js diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/test.ts similarity index 58% rename from dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/test.ts rename to dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/test.ts index aa7bc8f7aa17..24a2664154f5 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import type { Event, UserFeedback } from '@sentry/core'; +import type { Event, FeedbackEvent } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; @@ -7,17 +7,21 @@ import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; sentryTest('capture user feedback when captureException is called', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const data = (await getMultipleSentryEnvelopeRequests(page, 2, { url })) as (Event | UserFeedback)[]; + const data = (await getMultipleSentryEnvelopeRequests(page, 2, { url })) as (Event | FeedbackEvent)[]; expect(data).toHaveLength(2); const errorEvent = ('exception' in data[0] ? data[0] : data[1]) as Event; - const feedback = ('exception' in data[0] ? data[1] : data[0]) as UserFeedback; + const feedback = ('exception' in data[0] ? data[1] : data[0]) as FeedbackEvent; - expect(feedback).toEqual({ - comments: 'This feedback should be attached associated with the captured error', - email: 'john@doe.com', - event_id: errorEvent.event_id, - name: 'John Doe', - }); + expect(feedback.contexts).toEqual( + expect.objectContaining({ + feedback: { + associated_event_id: errorEvent.event_id, + message: 'This feedback should be attached associated with the captured error', + contact_email: 'john@doe.com', + name: 'John Doe', + }, + }), + ); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js similarity index 60% rename from dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/init.js rename to dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js index 805d6adc2e1e..b1cc371e5ec6 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js @@ -5,11 +5,11 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', beforeSend(event) { - Sentry.captureUserFeedback({ - event_id: event.event_id, + Sentry.captureFeedback({ + associatedEventId: event.event_id, name: 'John Doe', email: 'john@doe.com', - comments: 'This feedback should be attached associated with the captured message', + message: 'This feedback should be attached associated with the captured message', }); return event; }, diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/subject.js rename to dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/subject.js diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/test.ts similarity index 57% rename from dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/test.ts rename to dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/test.ts index 019a5d4a0326..2ae261759767 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import type { Event, UserFeedback } from '@sentry/core'; +import type { Event, FeedbackEvent } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; @@ -7,17 +7,21 @@ import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; sentryTest('capture user feedback when captureMessage is called', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const data = (await getMultipleSentryEnvelopeRequests(page, 2, { url })) as (Event | UserFeedback)[]; + const data = (await getMultipleSentryEnvelopeRequests(page, 2, { url })) as (Event | FeedbackEvent)[]; expect(data).toHaveLength(2); const errorEvent = ('exception' in data[0] ? data[0] : data[1]) as Event; - const feedback = ('exception' in data[0] ? data[1] : data[0]) as UserFeedback; + const feedback = ('exception' in data[0] ? data[1] : data[0]) as FeedbackEvent; - expect(feedback).toEqual({ - comments: 'This feedback should be attached associated with the captured message', - email: 'john@doe.com', - event_id: errorEvent.event_id, - name: 'John Doe', - }); + expect(feedback.contexts).toEqual( + expect.objectContaining({ + feedback: { + message: 'This feedback should be attached associated with the captured message', + contact_email: 'john@doe.com', + associated_event_id: errorEvent.event_id, + name: 'John Doe', + }, + }), + ); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/simple_feedback/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/simple_feedback/test.ts deleted file mode 100644 index 3d1253910e3d..000000000000 --- a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/simple_feedback/test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { expect } from '@playwright/test'; -import type { UserFeedback } from '@sentry/core'; - -import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; - -sentryTest('should capture simple user feedback', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData).toMatchObject({ - eventId: 'test_event_id', - email: 'test_email', - comments: 'test_comments', - name: 'test_name', - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/errors/subject.js b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/errors/subject.js new file mode 100644 index 000000000000..1b632e0a9289 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/errors/subject.js @@ -0,0 +1 @@ +Sentry.captureException(new Error('woot')); diff --git a/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/errors/test.ts b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/errors/test.ts new file mode 100644 index 000000000000..310085607b09 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/errors/test.ts @@ -0,0 +1,11 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should default user to {{auto}} on errors when sendDefaultPii: true', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await getFirstSentryEnvelopeRequest(page, url); + expect(eventData.user?.ip_address).toBe('{{auto}}'); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/init.js b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/init.js new file mode 100644 index 000000000000..b876cb8a3288 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + sendDefaultPii: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/performance/subject.js b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/performance/subject.js new file mode 100644 index 000000000000..8a509ca1d99d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/performance/subject.js @@ -0,0 +1 @@ +Sentry.startSpan({ name: 'woot' }, () => undefined); diff --git a/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/performance/test.ts b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/performance/test.ts new file mode 100644 index 000000000000..6e1f20826548 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/performance/test.ts @@ -0,0 +1,21 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; + +sentryTest( + 'should default user to {{auto}} on transactions when sendDefaultPii: true', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForTransactionRequestOnUrl(page, url); + const transaction = envelopeRequestParser(req); + expect(transaction.user?.ip_address).toBe('{{auto}}'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/replay/init.js b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/replay/init.js new file mode 100644 index 000000000000..b40eb0542e6b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/replay/init.js @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +window.Replay = Sentry.replayIntegration({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, + useCompression: false, + blockAllMedia: false, + unmask: ['.sentry-unmask, [data-sentry-unmask]'], +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + integrations: [window.Replay], + sendDefaultPii: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/replay/test.ts b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/replay/test.ts new file mode 100644 index 000000000000..4f4ad6e3003a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/replay/test.ts @@ -0,0 +1,24 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers'; + +sentryTest( + 'replay recording should contain default performance spans', + async ({ getLocalTestUrl, page, browserName }) => { + // We only test this against the NPM package and replay bundles + // and only on chromium as most performance entries are only available in chromium + if (shouldSkipReplayTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + const replayEvent = getReplayEvent(await reqPromise0); + + expect(replayEvent.user?.ip_address).toBe('{{auto}}'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/sessions/init.js b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/sessions/init.js new file mode 100644 index 000000000000..2003a9cf82eb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/sessions/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sendDefaultPii: true, + release: '1.0', +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/sessions/test.ts b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/sessions/test.ts new file mode 100644 index 000000000000..898e1cd9dbec --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/sendDefaultPii/sessions/test.ts @@ -0,0 +1,13 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should default user to {{auto}} on sessions when sendDefaultPii: true', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + const session = await getFirstSentryEnvelopeRequest(page, url); + expect((session as any).attrs.ip_address).toBe('{{auto}}'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js b/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js index d8c94f36fdd0..1f66b52852eb 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js @@ -4,4 +4,5 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', + sendDefaultPii: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setUser/ip_address_null/subject.js b/dev-packages/browser-integration-tests/suites/public-api/setUser/ip_address_null/subject.js new file mode 100644 index 000000000000..a59797f44212 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/setUser/ip_address_null/subject.js @@ -0,0 +1,6 @@ +Sentry.setUser({ + id: 'foo', + ip_address: null, +}); + +Sentry.captureMessage('first_user'); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setUser/ip_address_null/test.ts b/dev-packages/browser-integration-tests/suites/public-api/setUser/ip_address_null/test.ts new file mode 100644 index 000000000000..9ea891858749 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/setUser/ip_address_null/test.ts @@ -0,0 +1,17 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should allow to set ip_address to null', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.message).toBe('first_user'); + expect(eventData.user).toEqual({ + id: 'foo', + ip_address: null, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setUser/unset_user/test.ts b/dev-packages/browser-integration-tests/suites/public-api/setUser/unset_user/test.ts index ac3c06311bf9..6fbdf6491d69 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setUser/unset_user/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/setUser/unset_user/test.ts @@ -10,15 +10,21 @@ sentryTest('should unset user', async ({ getLocalTestUrl, page }) => { const eventData = await getMultipleSentryEnvelopeRequests(page, 3, { url }); expect(eventData[0].message).toBe('no_user'); - expect(eventData[0].user).toBeUndefined(); + + // because sendDefaultPii: true + expect(eventData[0].user).toEqual({ ip_address: '{{auto}}' }); expect(eventData[1].message).toBe('user'); - expect(eventData[1].user).toMatchObject({ + expect(eventData[1].user).toEqual({ id: 'foo', ip_address: 'bar', other_key: 'baz', }); expect(eventData[2].message).toBe('unset_user'); - expect(eventData[2].user).toBeUndefined(); + + // because sendDefaultPii: true + expect(eventData[2].user).toEqual({ + ip_address: '{{auto}}', + }); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setUser/update_user/test.ts b/dev-packages/browser-integration-tests/suites/public-api/setUser/update_user/test.ts index 38a18daf2c2f..b799022fa4e7 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setUser/update_user/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/setUser/update_user/test.ts @@ -10,13 +10,14 @@ sentryTest('should update user', async ({ getLocalTestUrl, page }) => { const eventData = await getMultipleSentryEnvelopeRequests(page, 2, { url }); expect(eventData[0].message).toBe('first_user'); - expect(eventData[0].user).toMatchObject({ + expect(eventData[0].user).toEqual({ id: 'foo', ip_address: 'bar', }); expect(eventData[1].message).toBe('second_user'); - expect(eventData[1].user).toMatchObject({ + expect(eventData[1].user).toEqual({ id: 'baz', + ip_address: '{{auto}}', }); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js index 56c0e05a269c..8509c200c15d 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js @@ -1,7 +1,7 @@ Sentry.getCurrentScope().setPropagationContext({ parentSpanId: '1234567890123456', - spanId: '123456789012345x', traceId: '12345678901234567890123456789012', + sampleRand: Math.random(), }); Sentry.startSpan({ name: 'test_span_1' }, () => undefined); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-mixed-transaction/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-mixed-transaction/test.ts index af87e11df37e..0663d16b6995 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-mixed-transaction/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-mixed-transaction/test.ts @@ -47,6 +47,7 @@ sentryTest( sampled: 'true', trace_id: traceId, transaction: 'outer', + sample_rand: expect.any(String), }, }); @@ -64,6 +65,7 @@ sentryTest( sampled: 'true', trace_id: traceId, transaction: 'outer', + sample_rand: expect.any(String), }, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone/test.ts index a37a50f28e04..ef1019d02b1d 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone/test.ts @@ -33,6 +33,7 @@ sentryTest('sends a segment span envelope', async ({ getLocalTestUrl, page }) => sampled: 'true', trace_id: traceId, transaction: 'standalone_segment_span', + sample_rand: expect.any(String), }, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js b/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js index d8c94f36fdd0..1f66b52852eb 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js @@ -4,4 +4,5 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', + sendDefaultPii: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/withScope/nested_scopes/test.ts b/dev-packages/browser-integration-tests/suites/public-api/withScope/nested_scopes/test.ts index be81e06a6e40..bf10eed2b504 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/withScope/nested_scopes/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/withScope/nested_scopes/test.ts @@ -10,22 +10,36 @@ sentryTest('should allow nested scoping', async ({ getLocalTestUrl, page }) => { const eventData = await getMultipleSentryEnvelopeRequests(page, 5, { url }); expect(eventData[0].message).toBe('root_before'); - expect(eventData[0].user).toMatchObject({ id: 'qux' }); + expect(eventData[0].user).toEqual({ + id: 'qux', + ip_address: '{{auto}}', // because sendDefaultPii: true + }); expect(eventData[0].tags).toBeUndefined(); expect(eventData[1].message).toBe('outer_before'); - expect(eventData[1].user).toMatchObject({ id: 'qux' }); + expect(eventData[1].user).toEqual({ + id: 'qux', + ip_address: '{{auto}}', // because sendDefaultPii: true + }); expect(eventData[1].tags).toMatchObject({ foo: false }); expect(eventData[2].message).toBe('inner'); - expect(eventData[2].user).toBeUndefined(); + expect(eventData[2].user).toEqual({ + ip_address: '{{auto}}', // because sendDefaultPii: true + }); expect(eventData[2].tags).toMatchObject({ foo: false, bar: 10 }); expect(eventData[3].message).toBe('outer_after'); - expect(eventData[3].user).toMatchObject({ id: 'baz' }); + expect(eventData[3].user).toEqual({ + id: 'baz', + ip_address: '{{auto}}', // because sendDefaultPii: true + }); expect(eventData[3].tags).toMatchObject({ foo: false }); expect(eventData[4].message).toBe('root_after'); - expect(eventData[4].user).toMatchObject({ id: 'qux' }); + expect(eventData[4].user).toEqual({ + id: 'qux', + ip_address: '{{auto}}', // because sendDefaultPii: true + }); expect(eventData[4].tags).toBeUndefined(); }); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts index b8b30184b754..ddd4be03e376 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts @@ -5,7 +5,7 @@ import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../. sentryTest('should capture replays offline', async ({ getLocalTestUrl, page }) => { // makeBrowserOfflineTransport is not included in any CDN bundles - if (shouldSkipReplayTest() || (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle'))) { + if (shouldSkipReplayTest() || process.env.PW_BUNDLE?.startsWith('bundle')) { sentryTest.skip(); } 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 384ee0071f64..a0deed767979 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts @@ -62,6 +62,7 @@ sentryTest( public_key: 'public', replay_id: replay.session?.id, sampled: 'true', + sample_rand: expect.any(String), }); }, ); @@ -108,6 +109,7 @@ sentryTest( trace_id: expect.stringMatching(/[a-f0-9]{32}/), public_key: 'public', sampled: 'true', + sample_rand: expect.any(String), }); }, ); @@ -161,6 +163,7 @@ sentryTest( public_key: 'public', replay_id: replay.session?.id, sampled: 'true', + sample_rand: expect.any(String), }); }, ); @@ -202,6 +205,7 @@ sentryTest( trace_id: expect.stringMatching(/[a-f0-9]{32}/), public_key: 'public', sampled: 'true', + sample_rand: expect.any(String), }); }, ); @@ -247,6 +251,7 @@ sentryTest('should add replay_id to error DSC while replay is active', async ({ ? { sample_rate: '1', sampled: 'true', + sample_rand: expect.any(String), } : {}), }); @@ -267,6 +272,7 @@ sentryTest('should add replay_id to error DSC while replay is active', async ({ ? { sample_rate: '1', sampled: 'true', + sample_rand: expect.any(String), } : {}), }); 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 a5f10c18d83f..ffe667d780dc 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts @@ -7,7 +7,7 @@ sentryTest( async ({ getLocalTestUrl, page, forceFlushReplay }) => { const bundle = process.env.PW_BUNDLE; - if (!bundle || !bundle.startsWith('bundle_') || bundle.includes('replay')) { + if (!bundle?.startsWith('bundle_') || bundle.includes('replay')) { sentryTest.skip(); } 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 7df2ab111f3f..8df888863ea2 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts @@ -7,7 +7,7 @@ sentryTest( async ({ getLocalTestUrl, page, forceFlushReplay }) => { const bundle = process.env.PW_BUNDLE; - if (!bundle || !bundle.startsWith('bundle_') || bundle.includes('replay')) { + if (!bundle?.startsWith('bundle_') || bundle.includes('replay')) { sentryTest.skip(); } diff --git a/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts index 3f1419d1615d..5a576bc1672d 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts @@ -2,14 +2,16 @@ import { expect } from '@playwright/test'; import type { SessionContext } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest, waitForSession } from '../../../utils/helpers'; sentryTest('should update session when an error is thrown.', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadSession = await getFirstSentryEnvelopeRequest(page, url); - const updatedSession = ( - await Promise.all([page.locator('#throw-error').click(), getFirstSentryEnvelopeRequest(page)]) - )[1]; + + const updatedSessionPromise = waitForSession(page); + await page.locator('#throw-error').click(); + const updatedSession = await updatedSessionPromise; expect(pageloadSession).toBeDefined(); expect(pageloadSession.init).toBe(true); @@ -25,9 +27,10 @@ sentryTest('should update session when an exception is captured.', async ({ getL const url = await getLocalTestUrl({ testDir: __dirname }); const pageloadSession = await getFirstSentryEnvelopeRequest(page, url); - const updatedSession = ( - await Promise.all([page.locator('#capture-exception').click(), getFirstSentryEnvelopeRequest(page)]) - )[1]; + + const updatedSessionPromise = waitForSession(page); + await page.locator('#capture-exception').click(); + const updatedSession = await updatedSessionPromise; expect(pageloadSession).toBeDefined(); expect(pageloadSession.init).toBe(true); diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js deleted file mode 100644 index 4958e35f2198..000000000000 --- a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js +++ /dev/null @@ -1,14 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '0.1', - // intentionally disabling this, we want to leverage the deprecated hub API - autoSessionTracking: false, -}); - -// simulate old startSessionTracking behavior -Sentry.getCurrentHub().startSession({ ignoreDuration: true }); -Sentry.getCurrentHub().captureSession(); diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts deleted file mode 100644 index 0dd12d17fe95..000000000000 --- a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Route } from '@playwright/test'; -import { expect } from '@playwright/test'; -import type { SessionContext } from '@sentry/core'; - -import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; - -sentryTest('should start a new session on pageload.', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - const session = await getFirstSentryEnvelopeRequest(page, url); - - expect(session).toBeDefined(); - expect(session.init).toBe(true); - expect(session.errors).toBe(0); - expect(session.status).toBe('ok'); -}); - -sentryTest('should start a new session with navigation.', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.route('**/foo', (route: Route) => route.continue({ url })); - - const initSession = await getFirstSentryEnvelopeRequest(page, url); - - await page.locator('#navigate').click(); - - const newSession = await getFirstSentryEnvelopeRequest(page, url); - - expect(newSession).toBeDefined(); - expect(newSession.init).toBe(true); - expect(newSession.errors).toBe(0); - expect(newSession.status).toBe('ok'); - expect(newSession.sid).toBeDefined(); - expect(initSession.sid).not.toBe(newSession.sid); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html index 09984cb0c488..7f7b0b159fee 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html @@ -5,7 +5,7 @@ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts index 39cad4b8f7d0..1d6e7044b007 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts @@ -43,6 +43,7 @@ sentryTest( sample_rate: '0.3232', trace_id: '123', public_key: 'public', + sample_rand: '0.42', }); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts index 6226ff75dbb9..7d2d949898c2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, @@ -10,27 +10,34 @@ import { import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('sets the source to custom when updating the transaction name', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest( + 'sets the source to custom when updating the transaction name with `span.updateName`', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } - const url = await getLocalTestUrl({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); - const traceContextData = eventData.contexts?.trace?.data; + const traceContextData = eventData.contexts?.trace?.data; - expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - }); + expect(traceContextData).toBeDefined(); - expect(eventData.transaction).toBe('new name'); + expect(eventData.transaction).toBe('new name'); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - expect(eventData.spans?.length).toBeGreaterThan(0); - expect(eventData.transaction_info?.source).toEqual('custom'); -}); + expect(traceContextData).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }); + + expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('custom'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js new file mode 100644 index 000000000000..1f0b64911a75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/subject.js new file mode 100644 index 000000000000..7f0ad0fea340 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/subject.js @@ -0,0 +1,4 @@ +const activeSpan = Sentry.getActiveSpan(); +const rootSpan = activeSpan && Sentry.getRootSpan(activeSpan); + +Sentry.updateSpanName(rootSpan, 'new name'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts new file mode 100644 index 000000000000..69094b38e4dd --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts @@ -0,0 +1,43 @@ +import { expect } from '@playwright/test'; +import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; + +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/browser'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'sets the source to custom when updating the transaction name with Sentry.updateSpanName', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + const traceContextData = eventData.contexts?.trace?.data; + + expect(traceContextData).toBeDefined(); + + expect(traceContextData).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }); + + expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + + expect(eventData.transaction).toBe('new name'); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('custom'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/init.js new file mode 100644 index 000000000000..c026daa1eed9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: integrations => { + integrations.push(Sentry.browserTracingIntegration()); + return integrations.filter(i => i.name !== 'BrowserSession'); + }, + tracesSampleRate: 0, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/subject.js new file mode 100644 index 000000000000..b7d62f8cfb95 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/subject.js @@ -0,0 +1,2 @@ +Sentry.captureException(new Error('test error')); +Sentry.captureException(new Error('test error 2')); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/template.html new file mode 100644 index 000000000000..22d155bf8648 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/template.html @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/test.ts new file mode 100644 index 000000000000..5bed055dbc0a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/test.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/browser'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('errors in TwP mode have same trace ID & span IDs', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const traceId = '12312012123120121231201212312012'; + const spanId = '1121201211212012'; + + const url = await getLocalTestUrl({ testDir: __dirname }); + const [event1, event2] = await getMultipleSentryEnvelopeRequests(page, 2, { url }); + + // Ensure these are the actual errors we care about + expect(event1.exception?.values?.[0].value).toContain('test error'); + expect(event2.exception?.values?.[0].value).toContain('test error'); + + const contexts1 = event1.contexts; + const { trace_id: traceId1, span_id: spanId1 } = contexts1?.trace || {}; + expect(traceId1).toEqual(traceId); + + // Span ID is a virtual span, not the propagated one + expect(spanId1).not.toEqual(spanId); + expect(spanId1).toMatch(/^[a-f0-9]{16}$/); + + const contexts2 = event2.contexts; + const { trace_id: traceId2, span_id: spanId2 } = contexts2?.trace || {}; + expect(traceId2).toEqual(traceId); + expect(spanId2).toMatch(/^[a-f0-9]{16}$/); + + expect(spanId2).toEqual(spanId1); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/init.js new file mode 100644 index 000000000000..c026daa1eed9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: integrations => { + integrations.push(Sentry.browserTracingIntegration()); + return integrations.filter(i => i.name !== 'BrowserSession'); + }, + tracesSampleRate: 0, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/subject.js new file mode 100644 index 000000000000..b7d62f8cfb95 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/subject.js @@ -0,0 +1,2 @@ +Sentry.captureException(new Error('test error')); +Sentry.captureException(new Error('test error 2')); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/test.ts new file mode 100644 index 000000000000..3048de92b2f1 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/test.ts @@ -0,0 +1,30 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/browser'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('errors in TwP mode have same trace ID & span IDs', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const [event1, event2] = await getMultipleSentryEnvelopeRequests(page, 2, { url }); + + // Ensure these are the actual errors we care about + expect(event1.exception?.values?.[0].value).toContain('test error'); + expect(event2.exception?.values?.[0].value).toContain('test error'); + + const contexts1 = event1.contexts; + const { trace_id: traceId1, span_id: spanId1 } = contexts1?.trace || {}; + expect(traceId1).toMatch(/^[a-f0-9]{32}$/); + expect(spanId1).toMatch(/^[a-f0-9]{16}$/); + + const contexts2 = event2.contexts; + const { trace_id: traceId2, span_id: spanId2 } = contexts2?.trace || {}; + expect(traceId2).toMatch(/^[a-f0-9]{32}$/); + expect(spanId2).toMatch(/^[a-f0-9]{16}$/); + + expect(traceId2).toEqual(traceId1); + expect(spanId2).toEqual(spanId1); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts index 7ce5f7195a5b..829d75924ac8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts @@ -48,6 +48,7 @@ sentryTest('updates the DSC when the txn name is updated and high-quality', asyn 'sentry-environment=production', 'sentry-public_key=public', 'sentry-release=1.1.1', + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), 'sentry-sample_rate=1', 'sentry-sampled=true', `sentry-trace_id=${traceId}`, @@ -62,6 +63,7 @@ sentryTest('updates the DSC when the txn name is updated and high-quality', asyn sample_rate: '1', sampled: 'true', trace_id: traceId, + sample_rand: expect.any(String), }); // 4 @@ -73,6 +75,7 @@ sentryTest('updates the DSC when the txn name is updated and high-quality', asyn 'sentry-environment=production', 'sentry-public_key=public', 'sentry-release=1.1.1', + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), 'sentry-sample_rate=1', 'sentry-sampled=true', `sentry-trace_id=${traceId}`, @@ -89,6 +92,7 @@ sentryTest('updates the DSC when the txn name is updated and high-quality', asyn sampled: 'true', trace_id: traceId, transaction: 'updated-root-span-1', + sample_rand: expect.any(String), }); // 7 @@ -100,6 +104,7 @@ sentryTest('updates the DSC when the txn name is updated and high-quality', asyn 'sentry-environment=production', 'sentry-public_key=public', 'sentry-release=1.1.1', + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), 'sentry-sample_rate=1', 'sentry-sampled=true', `sentry-trace_id=${traceId}`, @@ -116,6 +121,7 @@ sentryTest('updates the DSC when the txn name is updated and high-quality', asyn sampled: 'true', trace_id: traceId, transaction: 'updated-root-span-2', + sample_rand: expect.any(String), }); // 10 @@ -137,6 +143,7 @@ sentryTest('updates the DSC when the txn name is updated and high-quality', asyn sampled: 'true', trace_id: traceId, transaction: 'updated-root-span-2', + sample_rand: expect.any(String), }); expect(txnEvent.transaction).toEqual('updated-root-span-2'); @@ -181,5 +188,6 @@ async function captureErrorAndGetEnvelopeTraceHeader(page: Page): Promise { - const req = route.request(); - const headers = await req.allHeaders(); - - // headers.bar was set in fetch options (and should be sent) - expect(headers.bar).toBe('22'); - // headers.foo was set in init request object (and should be ignored) - expect(headers.foo).toBeUndefined(); - - return route.fulfill({ - status: 200, - body: 'ok', - }); - }); - - await getLocalTestUrl({ testDir: __dirname }); + const requestPromise = page.waitForRequest('http://example.com/api/test/'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const request = await requestPromise; + + const headers = await request.allHeaders(); + + // headers.bar was set in fetch options (and should be sent) + expect(headers.bar).toBe('22'); + // headers.foo was set in init request object (and should be ignored) + expect(headers.foo).toBeUndefined(); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts index bcbfa1890cdd..a295a69a1cf1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts @@ -1,24 +1,22 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; - import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } - const url = await getLocalTestUrl({ testDir: __dirname }); + await page.route('http://example.com/*', route => route.fulfill({ body: 'ok' })); - // Because we fetch from http://example.com, fetch will throw a CORS error in firefox and webkit. - // Chromium does not throw for cors errors. - // This means that we will intercept a dynamic amount of envelopes here. + const url = await getLocalTestUrl({ testDir: __dirname }); - // We will wait 500ms for all envelopes to be sent. Generally, in all browsers, the last sent - // envelope contains tracing data. - const envelopes = await getMultipleSentryEnvelopeRequests(page, 4, { url, timeout: 10000 }); - const tracingEvent = envelopes.find(event => event.type === 'transaction')!; // last envelope contains tracing data on all browsers + const req = await waitForTransactionRequestOnUrl(page, url); + const tracingEvent = envelopeRequestParser(req); const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); @@ -48,6 +46,8 @@ sentryTest('should attach `sentry-trace` header to fetch requests', async ({ get sentryTest.skip(); } + await page.route('http://example.com/*', route => route.fulfill({ body: 'ok' })); + const url = await getLocalTestUrl({ testDir: __dirname }); const requests = ( diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/init.js index 7cd076a052e5..092c43f75eac 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/init.js @@ -7,4 +7,5 @@ Sentry.init({ integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: ['http://example.com'], tracesSampleRate: 1, + autoSessionTracking: false, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts index 4288c6727ab7..c01797376b64 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts @@ -9,6 +9,8 @@ sentryTest('should create spans for XHR requests', async ({ getLocalTestUrl, pag sentryTest.skip(); } + await page.route('http://example.com/*', route => route.fulfill({ body: 'ok' })); + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts index cd1e79c211aa..ca6e9723ce65 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts @@ -10,7 +10,7 @@ import { shouldSkipTracingTest, } from '../../../../utils/helpers'; -sentryTest('creates a new trace on each navigation', async ({ getLocalTestUrl, page }) => { +sentryTest('creates a new trace and sample_rand on each navigation', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } @@ -49,6 +49,7 @@ sentryTest('creates a new trace on each navigation', async ({ getLocalTestUrl, p sample_rate: '1', sampled: 'true', trace_id: navigation1TraceContext?.trace_id, + sample_rand: expect.any(String), }); expect(navigation2TraceContext).toMatchObject({ @@ -64,9 +65,11 @@ sentryTest('creates a new trace on each navigation', async ({ getLocalTestUrl, p sample_rate: '1', sampled: 'true', trace_id: navigation2TraceContext?.trace_id, + sample_rand: expect.any(String), }); expect(navigation1TraceContext?.trace_id).not.toEqual(navigation2TraceContext?.trace_id); + expect(navigation1TraceHeader?.sample_rand).not.toEqual(navigation2TraceHeader?.sample_rand); }); sentryTest('error after navigation has navigation traceId', async ({ getLocalTestUrl, page }) => { @@ -101,6 +104,7 @@ sentryTest('error after navigation has navigation traceId', async ({ getLocalTes sample_rate: '1', sampled: 'true', trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), }); const errorEventPromise = getFirstSentryEnvelopeRequest( @@ -124,6 +128,7 @@ sentryTest('error after navigation has navigation traceId', async ({ getLocalTes sample_rate: '1', sampled: 'true', trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), }); }); @@ -168,6 +173,7 @@ sentryTest('error during navigation has new navigation traceId', async ({ getLoc sample_rate: '1', sampled: 'true', trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), }); const errorTraceContext = errorEvent?.contexts?.trace; @@ -182,6 +188,7 @@ sentryTest('error during navigation has new navigation traceId', async ({ getLoc sample_rate: '1', sampled: 'true', trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), }); }); @@ -234,6 +241,7 @@ sentryTest( sample_rate: '1', sampled: 'true', trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), }); const headers = request.headers(); @@ -242,7 +250,7 @@ sentryTest( const navigationTraceId = navigationTraceContext?.trace_id; expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`)); expect(headers['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true`, + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sampled=true,sentry-sample_rand=${navigationTraceHeader?.sample_rand},sentry-sample_rate=1`, ); }, ); @@ -296,6 +304,7 @@ sentryTest( sample_rate: '1', sampled: 'true', trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), }); const headers = request.headers(); @@ -304,7 +313,7 @@ sentryTest( const navigationTraceId = navigationTraceContext?.trace_id; expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`)); expect(headers['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true`, + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sampled=true,sentry-sample_rand=${navigationTraceHeader?.sample_rand},sentry-sample_rate=1`, ); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/template.html index 0dee204aef16..64b3a29fac28 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/template.html @@ -4,7 +4,7 @@ + content="sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=0.2,sentry-sampled=true,sentry-transaction=my-transaction,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.42"/> diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts index d102ad4c128e..d087dd0b32af 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts @@ -13,7 +13,7 @@ import { const META_TAG_TRACE_ID = '12345678901234567890123456789012'; const META_TAG_PARENT_SPAN_ID = '1234567890123456'; const META_TAG_BAGGAGE = - 'sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=0.2,sentry-sampled=true,sentry-transaction=my-transaction,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod'; + 'sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=0.2,sentry-sampled=true,sentry-transaction=my-transaction,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.42'; sentryTest( 'create a new trace for a navigation after the tag pageload trace', @@ -54,6 +54,7 @@ sentryTest( transaction: 'my-transaction', public_key: 'public', trace_id: META_TAG_TRACE_ID, + sample_rand: '0.42', }); expect(navigationEvent.type).toEqual('transaction'); @@ -71,9 +72,11 @@ sentryTest( sample_rate: '1', sampled: 'true', trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), }); expect(pageloadTraceContext?.trace_id).not.toEqual(navigationTraceContext?.trace_id); + expect(pageloadTraceHeader?.sample_rand).not.toEqual(navigationTraceHeader?.sample_rand); }, ); @@ -105,6 +108,7 @@ sentryTest('error after tag pageload has pageload traceId', async ({ getL transaction: 'my-transaction', public_key: 'public', trace_id: META_TAG_TRACE_ID, + sample_rand: '0.42', }); const errorEventPromise = getFirstSentryEnvelopeRequest( @@ -130,6 +134,7 @@ sentryTest('error after tag pageload has pageload traceId', async ({ getL transaction: 'my-transaction', public_key: 'public', trace_id: META_TAG_TRACE_ID, + sample_rand: '0.42', }); }); @@ -171,6 +176,7 @@ sentryTest('error during tag pageload has pageload traceId', async ({ get transaction: 'my-transaction', public_key: 'public', trace_id: META_TAG_TRACE_ID, + sample_rand: '0.42', }); expect(errorEvent.type).toEqual(undefined); @@ -188,6 +194,7 @@ sentryTest('error during tag pageload has pageload traceId', async ({ get transaction: 'my-transaction', public_key: 'public', trace_id: META_TAG_TRACE_ID, + sample_rand: '0.42', }); }); @@ -234,6 +241,7 @@ sentryTest( transaction: 'my-transaction', public_key: 'public', trace_id: META_TAG_TRACE_ID, + sample_rand: '0.42', }); const headers = request.headers(); @@ -287,6 +295,7 @@ sentryTest( transaction: 'my-transaction', public_key: 'public', trace_id: META_TAG_TRACE_ID, + sample_rand: '0.42', }); const headers = request.headers(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts index 00bbb26740de..4af462e26aca 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts @@ -49,6 +49,7 @@ sentryTest( sample_rate: '1', sampled: 'true', trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), }); expect(navigationTraceContext).toMatchObject({ @@ -64,6 +65,7 @@ sentryTest( sample_rate: '1', sampled: 'true', trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), }); expect(pageloadTraceContext?.span_id).not.toEqual(navigationTraceContext?.span_id); @@ -98,6 +100,7 @@ sentryTest('error after pageload has pageload traceId', async ({ getLocalTestUrl sample_rate: '1', sampled: 'true', trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), }); const errorEventPromise = getFirstSentryEnvelopeRequest( @@ -122,6 +125,7 @@ sentryTest('error after pageload has pageload traceId', async ({ getLocalTestUrl sample_rate: '1', sampled: 'true', trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), }); }); @@ -163,6 +167,7 @@ sentryTest('error during pageload has pageload traceId', async ({ getLocalTestUr sample_rate: '1', sampled: 'true', trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), }); const errorTraceContext = errorEvent?.contexts?.trace; @@ -179,6 +184,7 @@ sentryTest('error during pageload has pageload traceId', async ({ getLocalTestUr sample_rate: '1', sampled: 'true', trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), }); }); @@ -226,14 +232,15 @@ sentryTest( sample_rate: '1', sampled: 'true', trace_id: pageloadTraceId, + sample_rand: expect.any(String), }); const headers = request.headers(); // sampling decision is propagated from active span sampling decision expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`)); - expect(headers['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true`, + expect(headers['baggage']).toBe( + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${pageloadTraceHeader?.sample_rand},sentry-sample_rate=1`, ); }, ); @@ -282,14 +289,15 @@ sentryTest( sample_rate: '1', sampled: 'true', trace_id: pageloadTraceId, + sample_rand: expect.any(String), }); const headers = request.headers(); // sampling decision is propagated from active span sampling decision expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`)); - expect(headers['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true`, + expect(headers['baggage']).toBe( + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${pageloadTraceHeader?.sample_rand},sentry-sample_rate=1`, ); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts index 3ddca4787aee..a785327a0031 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts @@ -48,6 +48,7 @@ sentryTest( sample_rate: '1', sampled: 'true', trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), }); const transactionPromises = getMultipleSentryEnvelopeRequests( @@ -81,6 +82,7 @@ sentryTest( sampled: 'true', trace_id: newTraceTransactionTraceContext?.trace_id, transaction: 'new-trace', + sample_rand: expect.any(String), }); const oldTraceTransactionEventTraceContext = oldTraceTransactionEvent.contexts?.trace; @@ -96,6 +98,7 @@ sentryTest( sample_rate: '1', sampled: 'true', trace_id: oldTraceTransactionTraceHeaders?.trace_id, + sample_rand: expect.any(String), // transaction: 'old-trace', <-- this is not in the DSC because the DSC is continued from the pageload transaction // which does not have a `transaction` field because its source is URL. }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/template.html index 7cf101e4cf9e..d32f02cb6413 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/template.html @@ -5,7 +5,7 @@ + content="sentry-trace_id=12345678901234567890123456789012,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.42"/> diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts index 8fceec718447..bea3c10cbde5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts @@ -10,7 +10,7 @@ import { const META_TAG_TRACE_ID = '12345678901234567890123456789012'; const META_TAG_PARENT_SPAN_ID = '1234567890123456'; const META_TAG_BAGGAGE = - 'sentry-trace_id=12345678901234567890123456789012,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod'; + 'sentry-trace_id=12345678901234567890123456789012,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.42'; sentryTest('error on initial page has traceId from meta tag', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -41,6 +41,7 @@ sentryTest('error on initial page has traceId from meta tag', async ({ getLocalT public_key: 'public', release: '1.0.0', trace_id: META_TAG_TRACE_ID, + sample_rand: '0.42', }); }); @@ -72,6 +73,7 @@ sentryTest('error has new traceId after navigation', async ({ getLocalTestUrl, p public_key: 'public', release: '1.0.0', trace_id: META_TAG_TRACE_ID, + sample_rand: expect.any(String), }); const errorEventPromise2 = getFirstSentryEnvelopeRequest( diff --git a/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js b/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js new file mode 100644 index 000000000000..9247e1d8bcc2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/browser'; + +import { makeMultiplexedTransport } from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: makeMultiplexedTransport(Sentry.makeFetchTransport, ({ getEvent }) => { + const event = getEvent('event'); + + if (event.tags.to === 'a') { + return ['https://public@dsn.ingest.sentry.io/1337']; + } else if (event.tags.to === 'b') { + return ['https://public@dsn.ingest.sentry.io/1337']; + } else { + throw new Error('Unknown destination'); + } + }), +}); diff --git a/dev-packages/browser-integration-tests/suites/transport/multiplexed/subject.js b/dev-packages/browser-integration-tests/suites/transport/multiplexed/subject.js new file mode 100644 index 000000000000..89bb4b22eca1 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/subject.js @@ -0,0 +1,10 @@ +setTimeout(() => { + Sentry.withScope(scope => { + scope.setTag('to', 'a'); + Sentry.captureException(new Error('Error a')); + }); + Sentry.withScope(scope => { + scope.setTag('to', 'b'); + Sentry.captureException(new Error('Error b')); + }); +}, 0); diff --git a/dev-packages/browser-integration-tests/suites/transport/multiplexed/test.ts b/dev-packages/browser-integration-tests/suites/transport/multiplexed/test.ts new file mode 100644 index 000000000000..0bf274291df4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/test.ts @@ -0,0 +1,20 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../utils/helpers'; + +sentryTest('sends event to DSNs specified in makeMultiplexedTransport', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + const errorEvents = await getMultipleSentryEnvelopeRequests(page, 2, { envelopeType: 'event', url }); + + expect(errorEvents).toHaveLength(2); + + const [evt1, evt2] = errorEvents; + + const errorA = evt1?.tags?.to === 'a' ? evt1 : evt2; + const errorB = evt1?.tags?.to === 'b' ? evt1 : evt2; + + expect(errorA.tags?.to).toBe('a'); + expect(errorB.tags?.to).toBe('b'); +}); 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 9b6eb36fd0ea..c330c17be1f7 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 ({ getLocalTestUrl, 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?.startsWith('bundle')) { sentryTest.skip(); } diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index b9b4dcb4c1f3..77792d02b19c 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import type { Package } from '@sentry/core'; +import { type Package } from '@sentry/core'; import HtmlWebpackPlugin, { createHtmlTagObject } from 'html-webpack-plugin'; import type { Compiler } from 'webpack'; @@ -30,14 +30,14 @@ const useLoader = bundleKey.startsWith('loader'); const IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS: Record = { httpClientIntegration: 'httpclient', captureConsoleIntegration: 'captureconsole', - debugIntegration: 'debug', rewriteFramesIntegration: 'rewriteframes', contextLinesIntegration: 'contextlines', extraErrorDataIntegration: 'extraerrordata', reportingObserverIntegration: 'reportingobserver', - sessionTimingIntegration: 'sessiontiming', feedbackIntegration: 'feedback', moduleMetadataIntegration: 'modulemetadata', + // technically, this is not an integration, but let's add it anyway for simplicity + makeMultiplexedTransport: 'multiplexedtransport', }; const BUNDLE_PATHS: Record> = { diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index 03c654c22eb1..e89f5ae3c2f7 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -7,6 +7,7 @@ import type { Event, EventEnvelope, EventEnvelopeHeaders, + SessionContext, TransactionEvent, } from '@sentry/core'; @@ -157,7 +158,7 @@ export const countEnvelopes = async ( * @param {{ path?: string; content?: string }} impl * @return {*} {Promise} */ -async function runScriptInSandbox( +export async function runScriptInSandbox( page: Page, impl: { path?: string; @@ -178,7 +179,7 @@ async function runScriptInSandbox( * @param {string} [url] * @return {*} {Promise>} */ -async function getSentryEvents(page: Page, url?: string): Promise> { +export async function getSentryEvents(page: Page, url?: string): Promise> { if (url) { await page.goto(url); } @@ -250,6 +251,25 @@ export function waitForTransactionRequest( }); } +export async function waitForSession(page: Page): Promise { + const req = await page.waitForRequest(req => { + const postData = req.postData(); + if (!postData) { + return false; + } + + try { + const event = envelopeRequestParser(req); + + return typeof event.init === 'boolean' && event.started !== undefined; + } catch { + return false; + } + }); + + return envelopeRequestParser(req); +} + /** * We can only test tracing tests in certain bundles/packages: * - NPM (ESM, CJS) @@ -270,18 +290,6 @@ export function shouldSkipFeedbackTest(): boolean { return false; } -/** - * We can only test metrics tests in certain bundles/packages: - * - NPM (ESM, CJS) - * - CDN bundles that include tracing - * - * @returns `true` if we should skip the metrics test - */ -export function shouldSkipMetricsTest(): boolean { - const bundle = process.env.PW_BUNDLE as string | undefined; - return bundle != null && !bundle.includes('tracing') && !bundle.includes('esm') && !bundle.includes('cjs'); -} - /** * We only test feature flags integrations in certain bundles/packages: * - NPM (ESM, CJS) @@ -365,7 +373,7 @@ async function getMultipleRequests( /** * Wait and get multiple envelope requests at the given URL, or the current page */ -async function getMultipleSentryEnvelopeRequests( +export async function getMultipleSentryEnvelopeRequests( page: Page, count: number, options?: { @@ -386,7 +394,7 @@ async function getMultipleSentryEnvelopeRequests( * @param {string} [url] * @return {*} {Promise} */ -async function getFirstSentryEnvelopeRequest( +export async function getFirstSentryEnvelopeRequest( page: Page, url?: string, requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T, @@ -400,5 +408,3 @@ async function getFirstSentryEnvelopeRequest( return req; } - -export { runScriptInSandbox, getMultipleSentryEnvelopeRequests, getFirstSentryEnvelopeRequest, getSentryEvents }; diff --git a/dev-packages/browser-integration-tests/utils/replayHelpers.ts b/dev-packages/browser-integration-tests/utils/replayHelpers.ts index e090eba48200..1426030d594c 100644 --- a/dev-packages/browser-integration-tests/utils/replayHelpers.ts +++ b/dev-packages/browser-integration-tests/utils/replayHelpers.ts @@ -12,7 +12,7 @@ import type { fullSnapshotEvent, incrementalSnapshotEvent } from '@sentry-intern import { EventType } from '@sentry-internal/rrweb'; import type { ReplayEventWithTime } from '@sentry/browser'; import type { Breadcrumb, Event, ReplayEvent, ReplayRecordingMode } from '@sentry/core'; -import pako from 'pako'; +import { decompressSync, strFromU8 } from 'fflate'; import { envelopeRequestParser } from './helpers'; @@ -406,9 +406,9 @@ export const replayEnvelopeParser = (request: Request | null): unknown[] => { if (envelopeBytes[i] === 0x78 && envelopeBytes[i + 1] === 0x9c) { try { // We found a zlib-compressed payload - let's decompress it - const payload = envelopeBytes.slice(i); + const payload = (envelopeBytes as Buffer).subarray(i); // now we return the decompressed payload as JSON - const decompressedPayload = pako.inflate(payload as unknown as Uint8Array, { to: 'string' }); + const decompressedPayload = decompress(payload); return JSON.parse(decompressedPayload); } catch { // Let's log that something went wrong @@ -488,3 +488,12 @@ function getRequest(resOrReq: Request | Response): Request { // @ts-expect-error we check this return typeof resOrReq.request === 'function' ? (resOrReq as Response).request() : (resOrReq as Request); } + +/** Decompress a compressed data payload. */ +function decompress(data: Uint8Array): string { + if (!(data instanceof Uint8Array)) { + throw new Error(`Data passed to decompress is not a Uint8Array: ${data}`); + } + const decompressed = decompressSync(data); + return strFromU8(decompressed); +} diff --git a/dev-packages/e2e-tests/lib/getTestMatrix.ts b/dev-packages/e2e-tests/lib/getTestMatrix.ts index 342f20cf9820..e29f5c13043f 100644 --- a/dev-packages/e2e-tests/lib/getTestMatrix.ts +++ b/dev-packages/e2e-tests/lib/getTestMatrix.ts @@ -101,7 +101,7 @@ function addIncludesForTestApp( } function getSentryDependencies(appName: string): string[] { - const packageJson = getPackageJson(appName) || {}; + const packageJson = getPackageJson(appName); const dependencies = { ...packageJson.devDependencies, diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 6452d7752eba..958b1645e1c0 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -16,12 +16,12 @@ "clean": "rimraf tmp node_modules && yarn clean:test-applications && yarn clean:pnpm", "ci:build-matrix": "ts-node ./lib/getTestMatrix.ts", "ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true", - "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.sveltekit,pnpm-lock.yaml,.last-run.json,test-results}", + "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,pnpm-lock.yaml,.last-run.json,test-results}", "clean:pnpm": "pnpm store prune" }, "devDependencies": { "@types/glob": "8.0.0", - "@types/node": "^18.0.0", + "@types/node": "^18.19.1", "dotenv": "16.0.3", "esbuild": "0.20.0", "glob": "8.0.3", 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 index d437a1d43fdd..1e43d5c6c096 100644 --- 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 @@ -6,7 +6,10 @@ import { SampleComponent } from '../sample-component/sample-component.components selector: 'app-cancel', standalone: true, imports: [TraceModule, SampleComponent], - template: ``, + template: ` + + + `, }) @TraceClass({ name: 'ComponentTrackingComponent' }) export class ComponentTrackingComponent implements OnInit, AfterViewInit { 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 29c88a6108e2..03a715ce646c 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 @@ -191,7 +191,7 @@ test.describe('finish routing span', () => { }); test.describe('TraceDirective', () => { - test('creates a child tracingSpan with component name as span name on ngOnInit', async ({ page }) => { + test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => { const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; }); @@ -201,23 +201,36 @@ test.describe('TraceDirective', () => { // immediately navigate to a different route const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); - const traceDirectiveSpan = navigationTxn.spans?.find( + const traceDirectiveSpans = navigationTxn.spans?.filter( 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), - }), + expect(traceDirectiveSpans).toHaveLength(2); + expect(traceDirectiveSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // custom component name passed to trace directive + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // fallback selector name + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ]), ); }); }); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.npmrc b/dev-packages/e2e-tests/test-applications/angular-19/.npmrc similarity index 100% rename from dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.npmrc rename to dev-packages/e2e-tests/test-applications/angular-19/.npmrc diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts index d437a1d43fdd..a82e5b1acce6 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts @@ -3,10 +3,13 @@ import { TraceClass, TraceMethod, TraceModule } from '@sentry/angular'; import { SampleComponent } from '../sample-component/sample-component.components'; @Component({ - selector: 'app-cancel', + selector: 'app-component-tracking', standalone: true, imports: [TraceModule, SampleComponent], - template: ``, + template: ` + + + `, }) @TraceClass({ name: 'ComponentTrackingComponent' }) export class ComponentTrackingComponent implements OnInit, AfterViewInit { diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts index af85b8ffc405..c2cb2eca34b6 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts @@ -191,7 +191,7 @@ test.describe('finish routing span', () => { }); test.describe('TraceDirective', () => { - test('creates a child tracingSpan with component name as span name on ngOnInit', async ({ page }) => { + test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => { const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; }); @@ -201,23 +201,36 @@ test.describe('TraceDirective', () => { // immediately navigate to a different route const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); - const traceDirectiveSpan = navigationTxn.spans?.find( + const traceDirectiveSpans = navigationTxn.spans?.filter( 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), - }), + expect(traceDirectiveSpans).toHaveLength(2); + expect(traceDirectiveSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // custom component name passed to trace directive + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // fallback selector name + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ]), ); }); }); diff --git a/dev-packages/e2e-tests/test-applications/astro-4/package.json b/dev-packages/e2e-tests/test-applications/astro-4/package.json index 1aa316170a64..b80e408b3e32 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-4/package.json @@ -13,12 +13,12 @@ }, "dependencies": { "@astrojs/check": "0.9.2", - "@astrojs/node": "8.3.2", + "@astrojs/node": "8.3.4", "@playwright/test": "^1.46.0", "@sentry/astro": "* || latest", "@sentry-internal/test-utils": "link:../../../test-utils", "@spotlightjs/astro": "2.1.6", - "astro": "4.13.3", + "astro": "4.16.18", "typescript": "^5.5.4" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/astro-4/src/env.d.ts b/dev-packages/e2e-tests/test-applications/astro-4/src/env.d.ts index f964fe0cffd8..acef35f175aa 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/src/env.d.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/src/env.d.ts @@ -1 +1,2 @@ +/// /// diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts index 9a295f677d96..644afc377545 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts @@ -31,7 +31,6 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.browser', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), op: 'pageload', diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts index 8817b2b22aa7..c04bbb568f2e 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts @@ -36,7 +36,6 @@ test.describe('tracing in static/pre-rendered routes', () => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.browser', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), op: 'pageload', diff --git a/dev-packages/e2e-tests/test-applications/astro-5/package.json b/dev-packages/e2e-tests/test-applications/astro-5/package.json index 4e02fc855830..41724788f169 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-5/package.json @@ -17,5 +17,10 @@ "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/astro": "^8.42.0", "astro": "^5.0.3" + }, + "pnpm": { + "overrides": { + "esbuild": "0.24.0" + } } } diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts index 8c0e2c0c8850..2bcf6cbf2362 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts @@ -31,7 +31,6 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.browser', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), op: 'pageload', diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts index a6b288f4de71..fc396999d76e 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts @@ -33,7 +33,6 @@ test.describe('tracing in static routes with server islands', () => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.browser', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), op: 'pageload', @@ -75,7 +74,6 @@ test.describe('tracing in static routes with server islands', () => { data: expect.objectContaining({ 'sentry.op': 'http.server', 'sentry.origin': 'auto.http.astro', - 'sentry.sample_rate': 1, 'sentry.source': 'route', }), op: 'http.server', diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts index 9c202da53542..9db35c72a47d 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts @@ -36,7 +36,6 @@ test.describe('tracing in static/pre-rendered routes', () => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.browser', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), op: 'pageload', diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/package.json b/dev-packages/e2e-tests/test-applications/create-next-app/package.json index e91c0ee135e5..e70e7ed4c797 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-next-app/package.json @@ -13,13 +13,13 @@ }, "dependencies": { "@sentry/nextjs": "latest || *", - "@types/node": "18.11.17", + "@types/node": "^18.19.1", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", "next": "14.0.0", "react": "18.2.0", "react-dom": "18.2.0", - "typescript": "4.9.5" + "typescript": "~5.0.0" }, "devDependencies": { "@playwright/test": "^1.44.1", diff --git a/dev-packages/e2e-tests/test-applications/create-react-app/package.json b/dev-packages/e2e-tests/test-applications/create-react-app/package.json index 916a17260a2a..981123625b96 100644 --- a/dev-packages/e2e-tests/test-applications/create-react-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-react-app/package.json @@ -4,13 +4,13 @@ "private": true, "dependencies": { "@sentry/react": "latest || *", - "@types/node": "16.7.13", + "@types/node": "^18.19.1", "@types/react": "18.0.0", "@types/react-dom": "18.0.0", "react": "18.2.0", "react-dom": "18.2.0", "react-scripts": "5.0.1", - "typescript": "4.9.5" + "typescript": "~5.0.0" }, "scripts": { "start": "react-scripts start", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.eslintrc.cjs b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.eslintrc.cjs deleted file mode 100644 index 7adbd6f482f6..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.eslintrc.cjs +++ /dev/null @@ -1,79 +0,0 @@ -/** - * This is intended to be a basic starting point for linting in your app. - * It relies on recommended configs out of the box for simplicity, but you can - * and should modify this configuration to best suit your team's needs. - */ - -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, - env: { - browser: true, - commonjs: true, - es6: true, - }, - - // Base config - extends: ['eslint:recommended'], - - overrides: [ - // React - { - files: ['**/*.{js,jsx,ts,tsx}'], - plugins: ['react', 'jsx-a11y'], - extends: [ - 'plugin:react/recommended', - 'plugin:react/jsx-runtime', - 'plugin:react-hooks/recommended', - 'plugin:jsx-a11y/recommended', - ], - settings: { - react: { - version: 'detect', - }, - formComponents: ['Form'], - linkComponents: [ - { name: 'Link', linkAttribute: 'to' }, - { name: 'NavLink', linkAttribute: 'to' }, - ], - 'import/resolver': { - typescript: {}, - }, - }, - }, - - // Typescript - { - files: ['**/*.{ts,tsx}'], - plugins: ['@typescript-eslint', 'import'], - parser: '@typescript-eslint/parser', - settings: { - 'import/internal-regex': '^~/', - 'import/resolver': { - node: { - extensions: ['.ts', '.tsx'], - }, - typescript: { - alwaysTryTypes: true, - }, - }, - }, - extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/recommended', 'plugin:import/typescript'], - }, - - // Node - { - files: ['.eslintrc.cjs', 'server.js'], - env: { - node: true, - }, - }, - ], -}; diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.gitignore b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.gitignore deleted file mode 100644 index 3f7bf98da3e1..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules - -/.cache -/build -/public/build -.env diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.client.tsx deleted file mode 100644 index 46a0d015cdc0..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.client.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; -import * as Sentry from '@sentry/remix'; -import { StrictMode, startTransition, useEffect } from 'react'; -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, - useLocation, - useMatches, - }), - Sentry.replayIntegration(), - ], - // Performance Monitoring - tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! - 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 -}); - -startTransition(() => { - hydrateRoot( - document, - - - , - ); -}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.server.tsx deleted file mode 100644 index 4e1e9e0ba537..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.server.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import * as Sentry from '@sentry/remix'; - -import { PassThrough } from 'node:stream'; -import * as isbotModule from 'isbot'; - -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 { renderToPipeableStream } from 'react-dom/server'; - -installGlobals(); - -const ABORT_DELAY = 5_000; - -export const handleError = Sentry.wrapRemixHandleError; - -export default function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, - loadContext: AppLoadContext, -) { - return isBotRequest(request.headers.get('user-agent')) - ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) - : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext); -} - -// We have some Remix apps in the wild already running with isbot@3 so we need -// to maintain backwards compatibility even though we want new apps to use -// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev. -function isBotRequest(userAgent: string | null) { - if (!userAgent) { - return false; - } - - // isbot >= 3.8.0, >4 - if ('isbot' in isbotModule && typeof isbotModule.isbot === 'function') { - return isbotModule.isbot(userAgent); - } - - // isbot < 3.8.0 - if ('default' in isbotModule && typeof isbotModule.default === 'function') { - return isbotModule.default(userAgent); - } - - return false; -} - -function handleBotRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onAllReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set('Content-Type', 'text/html'); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }), - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - }, - ); - - setTimeout(abort, ABORT_DELAY); - }); -} - -function handleBrowserRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onShellReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set('Content-Type', 'text/html'); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }), - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - }, - ); - - setTimeout(abort, ABORT_DELAY); - }); -} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/root.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/root.tsx deleted file mode 100644 index 517a37a9d76b..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/root.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { cssBundleHref } from '@remix-run/css-bundle'; -import { LinksFunction, MetaFunction, json } from '@remix-run/node'; -import { - Links, - LiveReload, - Meta, - Outlet, - Scripts, - ScrollRestoration, - useLoaderData, - useRouteError, -} from '@remix-run/react'; -import { captureRemixErrorBoundaryError, withSentry } from '@sentry/remix'; -import type { SentryMetaArgs } from '@sentry/remix'; - -export const links: LinksFunction = () => [...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : [])]; - -export const loader = () => { - return json({ - ENV: { - SENTRY_DSN: process.env.E2E_TEST_DSN, - }, - }); -}; - -export const meta = ({ data }: SentryMetaArgs>) => { - return [ - { - env: data.ENV, - }, - { - name: 'sentry-trace', - content: data.sentryTrace, - }, - { - name: 'baggage', - content: data.sentryBaggage, - }, - ]; -}; - -export function ErrorBoundary() { - const error = useRouteError(); - const eventId = captureRemixErrorBoundaryError(error); - - return ( -
- ErrorBoundary Error - {eventId} -
- ); -} - -function App() { - const { ENV } = useLoaderData(); - - return ( - - - - - - - + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.client.test.ts index 4cb23e8b81df..a04ebf73f08d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.client.test.ts @@ -28,6 +28,47 @@ test.describe('client-side errors', async () => { }); }); + test('captures error thrown in NuxtErrorBoundary', async ({ page }) => { + const errorPromise = waitForError('nuxt-3', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown in Error Boundary'; + }); + + await page.goto(`/client-error`); + await page.locator('#error-in-error-boundary').click(); + + const error = await errorPromise; + + const expectedBreadcrumb = { + category: 'console', + message: 'Additional functionality in NuxtErrorBoundary', + }; + + const matchingBreadcrumb = error.breadcrumbs.find( + (breadcrumb: { category: string; message: string }) => + breadcrumb.category === expectedBreadcrumb.category && breadcrumb.message === expectedBreadcrumb.message, + ); + + expect(matchingBreadcrumb).toBeTruthy(); + expect(matchingBreadcrumb?.category).toBe(expectedBreadcrumb.category); + expect(matchingBreadcrumb?.message).toBe(expectedBreadcrumb.message); + + expect(error.transaction).toEqual('/client-error'); + expect(error.sdk.name).toEqual('sentry.javascript.nuxt'); + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown in Error Boundary', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); + test('shows parametrized route on button error', async ({ page }) => { const errorPromise = waitForError('nuxt-3', async errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Param Route Button'; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/client-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/client-error.vue index c2bdabfb4752..9ec99d1e7471 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/client-error.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/client-error.vue @@ -1,11 +1,16 @@ - - + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.client.test.ts index c887e255fe57..6694ae851df1 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.client.test.ts @@ -28,6 +28,47 @@ test.describe('client-side errors', async () => { }); }); + test('captures error thrown in NuxtErrorBoundary', async ({ page }) => { + const errorPromise = waitForError('nuxt-4', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown in Error Boundary'; + }); + + await page.goto(`/client-error`); + await page.locator('#error-in-error-boundary').click(); + + const error = await errorPromise; + + const expectedBreadcrumb = { + category: 'console', + message: 'Additional functionality in NuxtErrorBoundary', + }; + + const matchingBreadcrumb = error.breadcrumbs.find( + (breadcrumb: { category: string; message: string }) => + breadcrumb.category === expectedBreadcrumb.category && breadcrumb.message === expectedBreadcrumb.message, + ); + + expect(matchingBreadcrumb).toBeTruthy(); + expect(matchingBreadcrumb?.category).toBe(expectedBreadcrumb.category); + expect(matchingBreadcrumb?.message).toBe(expectedBreadcrumb.message); + + expect(error.transaction).toEqual('/client-error'); + expect(error.sdk.name).toEqual('sentry.javascript.nuxt'); + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown in Error Boundary', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); + test('shows parametrized route on button error', async ({ page }) => { const errorPromise = waitForError('nuxt-4', async errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Param Route Button'; diff --git a/dev-packages/e2e-tests/test-applications/react-17/package.json b/dev-packages/e2e-tests/test-applications/react-17/package.json index 9f6762325609..ab3022bb3c80 100644 --- a/dev-packages/e2e-tests/test-applications/react-17/package.json +++ b/dev-packages/e2e-tests/test-applications/react-17/package.json @@ -10,7 +10,7 @@ "react-dom": "17.0.2", "react-router-dom": "~6.3.0", "react-scripts": "5.0.1", - "typescript": "4.9.5" + "typescript": "~5.0.0" }, "scripts": { "build": "react-scripts build", diff --git a/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx index 49609a988202..aab492c7388b 100644 --- a/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx @@ -38,6 +38,7 @@ Sentry.init({ replaysOnErrorSampleRate: 0.0, tunnel: 'http://localhost:3031', + sendDefaultPii: true, }); const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); diff --git a/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts index 58e3df1ee8d6..14d8b8b21d65 100644 --- a/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts @@ -83,6 +83,7 @@ test('sends an INP span', async ({ page }) => { 'sentry.exclusive_time': expect.any(Number), replay_id: expect.any(String), 'user_agent.original': expect.stringContaining('Chrome'), + 'client.address': '{{auto}}', }, description: 'body > div#root > input#exception-button[type="button"]', op: 'ui.interaction.click', diff --git a/dev-packages/e2e-tests/test-applications/react-19/package.json b/dev-packages/e2e-tests/test-applications/react-19/package.json index 5de946437a44..1abc74715831 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/package.json +++ b/dev-packages/e2e-tests/test-applications/react-19/package.json @@ -6,13 +6,13 @@ "@sentry/react": "latest || *", "history": "4.9.0", "@types/history": "4.7.11", - "@types/node": "16.7.13", + "@types/node": "^18.19.1", "@types/react": "npm:types-react@rc", "@types/react-dom": "npm:types-react-dom@rc", "react": "19.0.0-rc-935180c7e0-20240524", "react-dom": "19.0.0-rc-935180c7e0-20240524", "react-scripts": "5.0.1", - "typescript": "4.9.5" + "typescript": "~5.0.0" }, "scripts": { "build": "react-scripts build", diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore new file mode 100644 index 000000000000..84634c973eeb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore @@ -0,0 +1,29 @@ +# 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/create-remix-app-v2-legacy/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc similarity index 100% rename from dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/.npmrc rename to dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json new file mode 100644 index 000000000000..a4e7dae6d1e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-create-browser-router-test", + "version": "0.1.0", + "private": true, + "dependencies": { + "@sentry/react": "latest || *", + "@types/node": "^18.19.1", + "@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": "~5.0.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 && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && 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.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "serve": "14.0.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/playwright.config.mjs rename to dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html b/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html new file mode 100644 index 000000000000..39da76522bea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html @@ -0,0 +1,24 @@ + + + + + + + + React App + + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts similarity index 69% rename from dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/globals.d.ts rename to dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts index 4130ac6a8a09..ffa61ca49acc 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/globals.d.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts @@ -1,7 +1,5 @@ interface Window { recordedTransactions?: string[]; capturedExceptionId?: string; - ENV: { - SENTRY_DSN: string; - }; + sentryReplayId?: string; } diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx new file mode 100644 index 000000000000..88f8cfa502ec --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx @@ -0,0 +1,66 @@ +import * as Sentry from '@sentry/react'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { + RouterProvider, + createBrowserRouter, + createRoutesFromChildren, + matchRoutes, + useLocation, + useNavigationType, +} from 'react-router-dom'; +import Index from './pages/Index'; +import User from './pages/User'; + +const replay = Sentry.replayIntegration(); + +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, + }), + replay, + ], + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + release: 'e2e-test', + + tunnel: 'http://localhost:3031', + + // Always capture replays, so we can test this properly + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + + debug: !!process.env.DEBUG, +}); + +const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter); + +const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: '/user/:id', + element: , + }, + ], + { + // We're testing whether this option is avoided in the integration + // We expect this to be ignored + initialEntries: ['/user/1'], + }, +); + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); + +root.render(); diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx new file mode 100644 index 000000000000..d6b71a1d1279 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx @@ -0,0 +1,23 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +const Index = () => { + return ( + <> + { + throw new Error('I am an error!'); + }} + /> + + navigate + + + ); +}; + +export default Index; diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx new file mode 100644 index 000000000000..62f0c2d17533 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx @@ -0,0 +1,8 @@ +// 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/react-create-browser-router/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts new file mode 100644 index 000000000000..6431bc5fc6b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs similarity index 69% rename from dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs index de34c7e196b6..be93e129284f 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'create-remix-app', + proxyServerName: 'react-create-browser-router', }); diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts new file mode 100644 index 000000000000..4a11f07410ab --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Captures exception correctly', async ({ page }) => { + const errorEventPromise = waitForError('react-create-browser-router', event => { + return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!'); + + expect(errorEvent.request).toEqual({ + headers: expect.any(Object), + url: 'http://localhost:3030/', + }); + + expect(errorEvent.transaction).toEqual('/'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts new file mode 100644 index 000000000000..5ecd098daf94 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts @@ -0,0 +1,78 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Captures a pageload transaction', async ({ page }) => { + const transactionEventPromise = waitForTransaction('react-create-browser-router', event => { + return event.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + expect(transactionEvent.contexts?.trace).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + deviceMemory: expect.any(String), + effectiveConnectionType: expect.any(String), + hardwareConcurrency: expect.any(String), + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.react.reactrouter_v6', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }), + op: 'pageload', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.pageload.react.reactrouter_v6', + }), + ); +}); + +test('Captures a navigation transaction', async ({ page }) => { + const transactionEventPromise = waitForTransaction('react-create-browser-router', event => { + return event.contexts?.trace?.op === 'navigation'; + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const transactionEvent = await transactionEventPromise; + expect(transactionEvent.contexts?.trace).toEqual({ + data: expect.objectContaining({ + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'navigation', + 'sentry.origin': 'auto.navigation.react.reactrouter_v6', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }), + op: 'navigation', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.navigation.react.reactrouter_v6', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/user/:id', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + expect(transactionEvent.spans).toEqual([]); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json new file mode 100644 index 000000000000..4cc95dc2689a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2018", + "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/react-create-hash-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json index e475fb505fc8..757b27c65b84 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json @@ -4,14 +4,14 @@ "private": true, "dependencies": { "@sentry/react": "latest || *", - "@types/node": "16.7.13", + "@types/node": "^18.19.1", "@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.4.2" + "typescript": "~5.0.0" }, "scripts": { "build": "react-scripts build", 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 638f38e2a3c4..2ad9490ccd57 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 @@ -41,7 +41,7 @@ Sentry.init({ debug: !!process.env.DEBUG, }); -const sentryCreateHashRouter = Sentry.wrapCreateBrowserRouter(createHashRouter); +const sentryCreateHashRouter = Sentry.wrapCreateBrowserRouterV6(createHashRouter); const router = sentryCreateHashRouter([ { diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore new file mode 100644 index 000000000000..84634c973eeb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore @@ -0,0 +1,29 @@ +# 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/create-remix-app/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc similarity index 100% rename from dev-packages/e2e-tests/test-applications/create-remix-app/.npmrc rename to dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json new file mode 100644 index 000000000000..dc6c9b4340f0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-create-memory-router-test", + "version": "0.1.0", + "private": true, + "dependencies": { + "@sentry/react": "latest || *", + "@types/node": "^18.19.1", + "@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": "~5.0.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 && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && 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.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "serve": "14.0.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/create-remix-app-legacy/playwright.config.mjs rename to dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html b/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html new file mode 100644 index 000000000000..39da76522bea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html @@ -0,0 +1,24 @@ + + + + + + + + React App + + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts similarity index 69% rename from dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/globals.d.ts rename to dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts index 4130ac6a8a09..ffa61ca49acc 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/globals.d.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts @@ -1,7 +1,5 @@ interface Window { recordedTransactions?: string[]; capturedExceptionId?: string; - ENV: { - SENTRY_DSN: string; - }; + sentryReplayId?: string; } diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx new file mode 100644 index 000000000000..f71572f9dc1f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx @@ -0,0 +1,65 @@ +import * as Sentry from '@sentry/react'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { + RouterProvider, + createMemoryRouter, + createRoutesFromChildren, + matchRoutes, + useLocation, + useNavigationType, +} from 'react-router-dom'; +import Index from './pages/Index'; +import User from './pages/User'; + +const replay = Sentry.replayIntegration(); + +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, + }), + replay, + ], + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + release: 'e2e-test', + + tunnel: 'http://localhost:3031', + + // Always capture replays, so we can test this properly + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + + debug: !!process.env.DEBUG, +}); + +const sentryCreateMemoryRouter = Sentry.wrapCreateMemoryRouterV6(createMemoryRouter); + +const router = sentryCreateMemoryRouter( + [ + { + path: '/', + element: , + }, + { + path: '/user/:id', + element: , + }, + ], + { + initialEntries: ['/', '/user/1', '/user/2'], + initialIndex: 2, + }, +); + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); + +root.render(); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx new file mode 100644 index 000000000000..b025f721e100 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx @@ -0,0 +1,19 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX +import * as React from 'react'; + +const Index = () => { + return ( + <> + { + throw new Error('I am an error!'); + }} + /> + + ); +}; + +export default Index; diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx new file mode 100644 index 000000000000..e54d6c604e2d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx @@ -0,0 +1,19 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +const User = () => { + return ( +
+ + Home + + + navigate + +

I am a blank page :)

; +
+ ); +}; + +export default User; diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts new file mode 100644 index 000000000000..6431bc5fc6b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs similarity index 69% rename from dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs index 1719aa397840..9c451610f4c7 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'create-remix-app-v2-legacy', + proxyServerName: 'react-create-memory-router', }); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts new file mode 100644 index 000000000000..9406ca63e30c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Captures exception correctly', async ({ page }) => { + const errorEventPromise = waitForError('react-create-memory-router', event => { + return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + }); + + await page.goto('/'); + + // We're on the user page, navigate back to the home page + const homeButton = page.locator('id=home-button'); + await homeButton.click(); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!'); + + expect(errorEvent.request).toEqual({ + headers: expect.any(Object), + url: 'http://localhost:3030/', + }); + + expect(errorEvent.transaction).toEqual('/'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts new file mode 100644 index 000000000000..7c75c395c3af --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts @@ -0,0 +1,75 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Captures a pageload transaction', async ({ page }) => { + const transactionEventPromise = waitForTransaction('react-create-memory-router', event => { + return event.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/user/:id', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + expect(transactionEvent.contexts?.trace).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.react.reactrouter_v6', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }), + op: 'pageload', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.pageload.react.reactrouter_v6', + }), + ); +}); + +test('Captures a navigation transaction', async ({ page }) => { + const transactionEventPromise = waitForTransaction('react-create-memory-router', event => { + return event.contexts?.trace?.op === 'navigation'; + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation-button'); + await linkElement.click(); + + const transactionEvent = await transactionEventPromise; + expect(transactionEvent.contexts?.trace).toEqual({ + data: expect.objectContaining({ + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'navigation', + 'sentry.origin': 'auto.navigation.react.reactrouter_v6', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }), + op: 'navigation', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.navigation.react.reactrouter_v6', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/user/:id', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + expect(transactionEvent.spans).toEqual([]); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json new file mode 100644 index 000000000000..4cc95dc2689a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2018", + "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/react-router-5/package.json b/dev-packages/e2e-tests/test-applications/react-router-5/package.json index 0b208b3f5a65..16c7f16df16d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-5/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-5/package.json @@ -6,7 +6,7 @@ "@sentry/react": "latest || *", "history": "4.9.0", "@types/history": "4.7.11", - "@types/node": "16.7.13", + "@types/node": "^18.19.1", "@types/react": "18.0.0", "@types/react-dom": "18.0.0", "@types/react-router": "5.1.20", @@ -15,7 +15,7 @@ "react-dom": "18.2.0", "react-router-dom": "5.3.4", "react-scripts": "5.0.1", - "typescript": "4.9.5" + "typescript": "~5.0.0" }, "scripts": { "build": "react-scripts build", diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json index ec6d7b05fee3..3c3323d2c4cc 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json @@ -11,7 +11,7 @@ "react-dom": "18.2.0", "react-router-dom": "^6.28.0", "react-scripts": "5.0.1", - "typescript": "4.9.5" + "typescript": "~5.0.0" }, "scripts": { "build": "react-scripts build", diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx index f6694a954915..581014169a78 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx @@ -3,6 +3,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter, + Outlet, Route, Routes, createRoutesFromChildren, @@ -48,17 +49,28 @@ const DetailsRoutes = () => ( ); +const DetailsRoutesAlternative = () => ( + + Details} /> + +); + const ViewsRoutes = () => ( Views} /> } /> + } /> ); const ProjectsRoutes = () => ( - }> - No Match Page} /> + }> + Project Page Root} /> + }> + } /> + + ); @@ -67,7 +79,7 @@ root.render( } /> - }> + } /> , ); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx index aa99b61f89ea..d2362c149f84 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx @@ -8,6 +8,9 @@ const Index = () => { navigate + + navigate old + ); }; diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts index 23bc0aaabe95..2f13b7cc1eac 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts @@ -10,6 +10,7 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = const rootSpan = await transactionPromise; + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); expect(rootSpan).toMatchObject({ contexts: { trace: { @@ -24,6 +25,30 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = }); }); +test('sends a pageload transaction with a parameterized URL - alternative route', async ({ page }) => { + const transactionPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/projects/234/old-views/234/567`); + + const rootSpan = await transactionPromise; + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.react.reactrouter_v6', + }, + }, + transaction: '/projects/:projectId/old-views/:viewId/:detailId', + transaction_info: { + source: 'route', + }, + }); +}); + test('sends a navigation transaction with a parameterized URL', async ({ page }) => { const pageloadTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; @@ -52,6 +77,8 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) const linkElement = page.locator('id=navigation'); const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); expect(navigationTxn).toMatchObject({ contexts: { trace: { @@ -65,3 +92,47 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) }, }); }); + +test('sends a navigation transaction with a parameterized URL - alternative route', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + const pageloadTxn = await pageloadTxnPromise; + + expect(pageloadTxn).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.react.reactrouter_v6', + }, + }, + transaction: '/', + transaction_info: { + source: 'route', + }, + }); + + const linkElement = page.locator('id=old-navigation'); + + const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.react.reactrouter_v6', + }, + }, + transaction: '/projects/:projectId/old-views/:viewId/:detailId', + transaction_info: { + source: 'route', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json index ca78e6af7310..7f68ec2a7ec4 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json @@ -10,7 +10,7 @@ "react-dom": "18.2.0", "react-router-dom": "^6.4.1", "react-scripts": "5.0.1", - "typescript": "4.9.5" + "typescript": "~5.0.0" }, "scripts": { "build": "react-scripts build", 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 b69dbff7dc48..3fe4310a8470 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 @@ -39,7 +39,7 @@ Sentry.init({ tunnel: 'http://localhost:3031', // proxy server }); -const useSentryRoutes = Sentry.wrapUseRoutes(useRoutes); +const useSentryRoutes = Sentry.wrapUseRoutesV6(useRoutes); function App() { return useSentryRoutes([ diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/package.json b/dev-packages/e2e-tests/test-applications/react-router-6/package.json index d086c765091c..575f417e2bc2 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6/package.json @@ -11,7 +11,7 @@ "react-dom": "18.2.0", "react-router-dom": "^6.4.1", "react-scripts": "5.0.1", - "typescript": "4.9.5" + "typescript": "~5.0.0" }, "scripts": { "build": "react-scripts build", diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx index 8c219563e5a4..76884645c4c0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx @@ -40,6 +40,7 @@ Sentry.init({ replaysOnErrorSampleRate: 0.0, tunnel: 'http://localhost:3031', + sendDefaultPii: true, }); const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6/tests/transactions.test.ts index 555e0655c52e..ae9ff366abd4 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6/tests/transactions.test.ts @@ -83,6 +83,7 @@ test('sends an INP span', async ({ page }) => { 'sentry.exclusive_time': expect.any(Number), replay_id: expect.any(String), 'user_agent.original': expect.stringContaining('Chrome'), + 'client.address': '{{auto}}', }, description: 'body > div#root > input#exception-button[type="button"]', op: 'ui.interaction.click', diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json index 1313fe2eed0e..1c505f3195a3 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json @@ -15,7 +15,7 @@ "@sentry-internal/test-utils": "link:../../../test-utils", "vite": "^6.0.1", "@vitejs/plugin-react": "^4.3.4", - "typescript": "4.9.5" + "typescript": "~5.0.0" }, "scripts": { "build": "vite build", @@ -56,5 +56,10 @@ "label": "react-router-7-spa (TS 3.8)" } ] + }, + "pnpm": { + "overrides": { + "esbuild": "0.24.0" + } } } diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx index a49c2c35de9d..baf12f7ff574 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx @@ -39,6 +39,7 @@ Sentry.init({ replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 0.0, tunnel: 'http://localhost:3031', + sendDefaultPii: true, }); const SentryRoutes = Sentry.withSentryReactRouterV7Routing(Routes); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts index c915d3694742..f0c7c680d07e 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts @@ -83,6 +83,7 @@ test('sends an INP span', async ({ page }) => { 'sentry.exclusive_time': expect.any(Number), replay_id: expect.any(String), 'user_agent.original': expect.stringContaining('Chrome'), + 'client.address': '{{auto}}', }, description: 'body > div#root > input#exception-button[type="button"]', op: 'ui.interaction.click', diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json index 836707b3017f..35b01833874a 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json @@ -4,14 +4,14 @@ "private": true, "dependencies": { "@sentry/react": "latest || *", - "@types/node": "16.7.13", + "@types/node": "^18.19.1", "@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" + "typescript": "~5.0.0" }, "scripts": { "build": "react-scripts build", diff --git a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json index cbb7afd9d09c..0c727d46de50 100644 --- a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json +++ b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json @@ -21,7 +21,7 @@ "postcss": "^8.4.33", "solid-devtools": "^0.29.2", "tailwindcss": "^3.4.1", - "vite": "^5.4.10", + "vite": "^5.4.11", "vite-plugin-solid": "^2.8.2" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/solid/package.json b/dev-packages/e2e-tests/test-applications/solid/package.json index bb37aa10f263..d61ac0a0a322 100644 --- a/dev-packages/e2e-tests/test-applications/solid/package.json +++ b/dev-packages/e2e-tests/test-applications/solid/package.json @@ -21,7 +21,7 @@ "postcss": "^8.4.33", "solid-devtools": "^0.29.2", "tailwindcss": "^3.4.1", - "vite": "^5.4.10", + "vite": "^5.4.11", "vite-plugin-solid": "^2.8.2" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore new file mode 100644 index 000000000000..a51ed3c20c8d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore @@ -0,0 +1,46 @@ + +dist +.solid +.output +.vercel +.netlify +.vinxi + +# Environment +.env +.env*.local + +# dependencies +/node_modules +/.pnp +.pnp.js + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# testing +/coverage + +# 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/node-nestjs-basic/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-nestjs-basic/.npmrc rename to dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md new file mode 100644 index 000000000000..9a141e9c2f0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md @@ -0,0 +1,45 @@ +# SolidStart + +Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); + +## Creating a project + +```bash +# create a new project in the current directory +npm init solid@latest + +# create a new project in my-app +npm init solid@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a +development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +Solid apps are built with _presets_, which optimise your project for deployment to different environments. + +By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add +it to the `devDependencies` in `package.json` and specify in your `app.config.js`. + +## Testing + +Tests are written with `vitest`, `@solidjs/testing-library` and `@testing-library/jest-dom` to extend expect with some +helpful custom matchers. + +To run them, simply start: + +```sh +npm test +``` + +## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts new file mode 100644 index 000000000000..f41b1cb186ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts @@ -0,0 +1,11 @@ +import { withSentry } from '@sentry/solidstart'; +import { defineConfig } from '@solidjs/start/config'; + +export default defineConfig( + withSentry( + {}, + { + autoInjectServerSentry: 'experimental_dynamic-import', + }, + ), +); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json new file mode 100644 index 000000000000..62393e038dce --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json @@ -0,0 +1,37 @@ +{ + "name": "solidstart-dynamic-import-e2e-testapp", + "version": "0.0.0", + "scripts": { + "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", + "dev": "vinxi dev", + "build": "vinxi build && sh ./post_build.sh", + "preview": "HOST=localhost PORT=3030 vinxi start", + "test:prod": "TEST_ENV=production playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:prod" + }, + "type": "module", + "dependencies": { + "@sentry/solidstart": "latest || *" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.13.4", + "@solidjs/start": "^1.0.2", + "@solidjs/testing-library": "^0.8.7", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/user-event": "^14.5.2", + "@vitest/ui": "^1.5.0", + "jsdom": "^24.0.0", + "solid-js": "1.8.17", + "typescript": "^5.4.5", + "vinxi": "^0.4.0", + "vite": "^5.4.10", + "vite-plugin-solid": "^2.10.2", + "vitest": "^1.5.0" + }, + "overrides": { + "@vercel/nft": "0.27.4" + } +} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs similarity index 74% rename from dev-packages/e2e-tests/test-applications/create-remix-app/playwright.config.mjs rename to dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs index 31f2b913b58b..395acfc282f9 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs @@ -1,7 +1,8 @@ import { getPlaywrightConfig } from '@sentry-internal/test-utils'; const config = getPlaywrightConfig({ - startCommand: `pnpm start`, + startCommand: 'pnpm preview', + port: 3030, }); export default config; diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh new file mode 100644 index 000000000000..6ed67c9afb8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh @@ -0,0 +1,8 @@ +# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved. + +# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules` +# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm +# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is +# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher). +cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules +cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico new file mode 100644 index 000000000000..fb282da0719e Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico differ diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/app.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/app.tsx new file mode 100644 index 000000000000..3eb85218b575 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/app.tsx @@ -0,0 +1,22 @@ +import { withSentryRouterRouting } from '@sentry/solidstart/solidrouter'; +import { MetaProvider, Title } from '@solidjs/meta'; +import { Router } from '@solidjs/router'; +import { FileRoutes } from '@solidjs/start/router'; +import { Suspense } from 'solid-js'; + +const SentryRouter = withSentryRouterRouting(Router); + +export default function App() { + return ( + ( + + SolidStart - with Vitest + {props.children} + + )} + > + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx new file mode 100644 index 000000000000..11087fbb5918 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx @@ -0,0 +1,18 @@ +// @refresh reload +import * as Sentry from '@sentry/solidstart'; +import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter'; +import { StartClient, mount } from '@solidjs/start/client'; + +Sentry.init({ + // We can't use env variables here, seems like they are stripped + // out in production builds. + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + integrations: [solidRouterBrowserTracingIntegration()], + tunnel: 'http://localhost:3031/', // proxy server + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + debug: !!import.meta.env.DEBUG, +}); + +mount(() => , document.getElementById('app')!); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx new file mode 100644 index 000000000000..276935366318 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { StartServer, createHandler } from '@solidjs/start/server'; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.mjs rename to dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx new file mode 100644 index 000000000000..ddd970944bf3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx @@ -0,0 +1,9 @@ +import { A } from '@solidjs/router'; + +export default function BackNavigation() { + return ( +
+ User 6 + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx new file mode 100644 index 000000000000..5e405e8c4e40 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx @@ -0,0 +1,15 @@ +export default function ClientErrorPage() { + return ( +
+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx new file mode 100644 index 000000000000..b22607667e7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx @@ -0,0 +1,64 @@ +import * as Sentry from '@sentry/solidstart'; +import type { ParentProps } from 'solid-js'; +import { ErrorBoundary, createSignal, onMount } from 'solid-js'; + +const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary); + +const [count, setCount] = createSignal(1); +const [caughtError, setCaughtError] = createSignal(false); + +export default function ErrorBoundaryTestPage() { + return ( + + {caughtError() && ( + + )} +
+
+ +
+
+
+ ); +} + +function Throw(props: { error: string }) { + onMount(() => { + throw new Error(props.error); + }); + return null; +} + +function SampleErrorBoundary(props: ParentProps) { + return ( + ( +
+

Error Boundary Fallback

+
+ {error.message} +
+ +
+ )} + > + {props.children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx new file mode 100644 index 000000000000..9a0b22cc38c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx @@ -0,0 +1,31 @@ +import { A } from '@solidjs/router'; + +export default function Home() { + return ( + <> +

Welcome to Solid Start

+

+ Visit docs.solidjs.com/solid-start to read the documentation +

+ + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx new file mode 100644 index 000000000000..05dce5e10a56 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx @@ -0,0 +1,17 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + throw new Error('Error thrown from Solid Start E2E test app server route'); + + return { prefecture: 'Kanagawa' }; + }); +}; + +export default function ServerErrorPage() { + const data = createAsync(() => getPrefecture()); + + return
Prefecture: {data()?.prefecture}
; +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx new file mode 100644 index 000000000000..22abd3ba8803 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx @@ -0,0 +1,21 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync, useParams } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + return { prefecture: 'Ehime' }; + }); +}; +export default function User() { + const params = useParams(); + const userData = createAsync(() => getPrefecture()); + + return ( +
+ User ID: {params.id} +
+ Prefecture: {userData()?.prefecture} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs similarity index 70% rename from dev-packages/e2e-tests/test-applications/create-remix-app-legacy/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs index b438dfab2193..343e434e030b 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'create-remix-app-legacy', + proxyServerName: 'solidstart-dynamic-import', }); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts new file mode 100644 index 000000000000..599b5c121455 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts @@ -0,0 +1,92 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('captures an exception', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + // The first page load causes a hydration error on the dev server sometimes - a reload works around this + await page.reload(); + await page.locator('#caughtErrorBtn').click(); + const errorEvent = await errorEventPromise; + + expect(errorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); + +test('captures a second exception after resetting the boundary', async ({ page }) => { + const firstErrorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + await page.locator('#caughtErrorBtn').click(); + const firstErrorEvent = await firstErrorEventPromise; + + expect(firstErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); + + const secondErrorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.locator('#errorBoundaryResetBtn').click(); + await page.locator('#caughtErrorBtn').click(); + const secondErrorEvent = await secondErrorEventPromise; + + expect(secondErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts new file mode 100644 index 000000000000..3a1b3ad4b812 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', () => { + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError('solidstart-dynamic-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Uncaught error thrown from Solid Start E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Uncaught error thrown from Solid Start E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + transaction: '/client-error', + }); + expect(error.transaction).toEqual('/client-error'); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts new file mode 100644 index 000000000000..7ef5cd0e07de --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', () => { + test('captures server action error', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Solid Start E2E test app server route'; + }); + + await page.goto(`/server-error`); + + const error = await errorEventPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Solid Start E2E test app server route', + mechanism: { + type: 'solidstart', + handled: false, + }, + }, + ], + }, + transaction: 'GET /server-error', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts new file mode 100644 index 000000000000..63f97d519cf8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return transactionEvent?.transaction === '/' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + const pageloadTransaction = await transactionPromise; + + expect(pageloadTransaction).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + }, + }, + transaction: '/', + transaction_info: { + source: 'url', + }, + }); +}); + +test('sends a navigation transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/5' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await page.locator('#navLink').click(); + const navigationTransaction = await transactionPromise; + + expect(navigationTransaction).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/5', + transaction_info: { + source: 'url', + }, + }); +}); + +test('updates the transaction when using the back button', async ({ page }) => { + // Solid Router sends a `-1` navigation when using the back button. + // The sentry solidRouterBrowserTracingIntegration tries to update such + // transactions with the proper name once the `useLocation` hook triggers. + const navigationTxnPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/6' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/back-navigation`); + await page.locator('#navLink').click(); + const navigationTxn = await navigationTxnPromise; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/6', + transaction_info: { + source: 'url', + }, + }); + + const backNavigationTxnPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return ( + transactionEvent?.transaction === '/back-navigation' && transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + await page.goBack(); + const backNavigationTxn = await backNavigationTxnPromise; + + expect(backNavigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/back-navigation', + transaction_info: { + source: 'url', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts new file mode 100644 index 000000000000..c300014bf012 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', transactionEvent => { + return transactionEvent?.transaction === 'GET /users/6'; + }); + + await page.goto('/users/6'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); + +test('sends a server action transaction on client navigation', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', transactionEvent => { + return transactionEvent?.transaction === 'POST getPrefecture'; + }); + + await page.goto('/'); + await page.locator('#navLink').click(); + await page.waitForURL('/users/5'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json new file mode 100644 index 000000000000..6f11292cc5d8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vinxi/types/client", "vitest/globals", "@testing-library/jest-dom"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts new file mode 100644 index 000000000000..6c2b639dc300 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts @@ -0,0 +1,10 @@ +import solid from 'vite-plugin-solid'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [solid()], + resolve: { + conditions: ['development', 'browser'], + }, + envPrefix: 'PUBLIC_', +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-spa/app.config.ts index d329d6066fc7..103ecb09a469 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/app.config.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/app.config.ts @@ -1,9 +1,9 @@ -import { sentrySolidStartVite } from '@sentry/solidstart'; +import { withSentry } from '@sentry/solidstart'; import { defineConfig } from '@solidjs/start/config'; -export default defineConfig({ - ssr: false, - vite: { - plugins: [sentrySolidStartVite()], - }, -}); +export default defineConfig( + withSentry({ + ssr: false, + middleware: './src/middleware.ts', + }), +); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json index f4ff0802e159..9495309f0464 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json @@ -3,9 +3,9 @@ "version": "0.0.0", "scripts": { "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", - "dev": "NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi dev", - "build": "vinxi build && sh ./post_build.sh", - "preview": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start", + "build": "vinxi build && sh post_build.sh", + "preview": "HOST=localhost PORT=3030 vinxi start", + "start:import": "HOST=localhost PORT=3030 node --import ./.output/server/instrument.server.mjs .output/server/index.mjs", "test:prod": "TEST_ENV=production playwright test", "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" @@ -27,7 +27,7 @@ "solid-js": "1.8.17", "typescript": "^5.4.5", "vinxi": "^0.4.0", - "vite": "^5.2.8", + "vite": "^5.4.11", "vite-plugin-solid": "^2.10.2", "vitest": "^1.5.0" }, diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart-spa/playwright.config.mjs index 395acfc282f9..ee2ee42980b8 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/playwright.config.mjs @@ -1,7 +1,7 @@ import { getPlaywrightConfig } from '@sentry-internal/test-utils'; const config = getPlaywrightConfig({ - startCommand: 'pnpm preview', + startCommand: 'pnpm start:import', port: 3030, }); diff --git a/dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.mjs rename to dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.ts diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/src/middleware.ts b/dev-packages/e2e-tests/test-applications/solidstart-spa/src/middleware.ts new file mode 100644 index 000000000000..88123a035fb6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/src/middleware.ts @@ -0,0 +1,6 @@ +import { sentryBeforeResponseMiddleware } from '@sentry/solidstart'; +import { createMiddleware } from '@solidjs/start/middleware'; + +export default createMiddleware({ + onBeforeResponse: [sentryBeforeResponseMiddleware()], +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore new file mode 100644 index 000000000000..a51ed3c20c8d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore @@ -0,0 +1,46 @@ + +dist +.solid +.output +.vercel +.netlify +.vinxi + +# Environment +.env +.env*.local + +# dependencies +/node_modules +/.pnp +.pnp.js + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# testing +/coverage + +# 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/node-nestjs-distributed-tracing/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/.npmrc rename to dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md new file mode 100644 index 000000000000..9a141e9c2f0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md @@ -0,0 +1,45 @@ +# SolidStart + +Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); + +## Creating a project + +```bash +# create a new project in the current directory +npm init solid@latest + +# create a new project in my-app +npm init solid@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a +development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +Solid apps are built with _presets_, which optimise your project for deployment to different environments. + +By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add +it to the `devDependencies` in `package.json` and specify in your `app.config.js`. + +## Testing + +Tests are written with `vitest`, `@solidjs/testing-library` and `@testing-library/jest-dom` to extend expect with some +helpful custom matchers. + +To run them, simply start: + +```sh +npm test +``` + +## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts new file mode 100644 index 000000000000..e4e73e9fc570 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts @@ -0,0 +1,11 @@ +import { withSentry } from '@sentry/solidstart'; +import { defineConfig } from '@solidjs/start/config'; + +export default defineConfig( + withSentry( + {}, + { + autoInjectServerSentry: 'top-level-import', + }, + ), +); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json new file mode 100644 index 000000000000..559477a58baa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json @@ -0,0 +1,37 @@ +{ + "name": "solidstart-top-level-import-e2e-testapp", + "version": "0.0.0", + "scripts": { + "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", + "dev": "vinxi dev", + "build": "vinxi build && sh ./post_build.sh", + "preview": "HOST=localhost PORT=3030 vinxi start", + "test:prod": "TEST_ENV=production playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:prod" + }, + "type": "module", + "dependencies": { + "@sentry/solidstart": "latest || *" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.13.4", + "@solidjs/start": "^1.0.2", + "@solidjs/testing-library": "^0.8.7", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/user-event": "^14.5.2", + "@vitest/ui": "^1.5.0", + "jsdom": "^24.0.0", + "solid-js": "1.8.17", + "typescript": "^5.4.5", + "vinxi": "^0.4.0", + "vite": "^5.4.11", + "vite-plugin-solid": "^2.10.2", + "vitest": "^1.5.0" + }, + "overrides": { + "@vercel/nft": "0.27.4" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs similarity index 74% rename from dev-packages/e2e-tests/test-applications/node-nestjs-basic/playwright.config.mjs rename to dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs index 31f2b913b58b..395acfc282f9 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs @@ -1,7 +1,8 @@ import { getPlaywrightConfig } from '@sentry-internal/test-utils'; const config = getPlaywrightConfig({ - startCommand: `pnpm start`, + startCommand: 'pnpm preview', + port: 3030, }); export default config; diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh new file mode 100644 index 000000000000..6ed67c9afb8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh @@ -0,0 +1,8 @@ +# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved. + +# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules` +# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm +# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is +# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher). +cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules +cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico new file mode 100644 index 000000000000..fb282da0719e Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico differ diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/app.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/app.tsx new file mode 100644 index 000000000000..3eb85218b575 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/app.tsx @@ -0,0 +1,22 @@ +import { withSentryRouterRouting } from '@sentry/solidstart/solidrouter'; +import { MetaProvider, Title } from '@solidjs/meta'; +import { Router } from '@solidjs/router'; +import { FileRoutes } from '@solidjs/start/router'; +import { Suspense } from 'solid-js'; + +const SentryRouter = withSentryRouterRouting(Router); + +export default function App() { + return ( + ( + + SolidStart - with Vitest + {props.children} + + )} + > + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx new file mode 100644 index 000000000000..11087fbb5918 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx @@ -0,0 +1,18 @@ +// @refresh reload +import * as Sentry from '@sentry/solidstart'; +import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter'; +import { StartClient, mount } from '@solidjs/start/client'; + +Sentry.init({ + // We can't use env variables here, seems like they are stripped + // out in production builds. + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + integrations: [solidRouterBrowserTracingIntegration()], + tunnel: 'http://localhost:3031/', // proxy server + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + debug: !!import.meta.env.DEBUG, +}); + +mount(() => , document.getElementById('app')!); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx new file mode 100644 index 000000000000..276935366318 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { StartServer, createHandler } from '@solidjs/start/server'; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts new file mode 100644 index 000000000000..3dd5d8933b7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/solidstart'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server + debug: !!process.env.DEBUG, +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx new file mode 100644 index 000000000000..ddd970944bf3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx @@ -0,0 +1,9 @@ +import { A } from '@solidjs/router'; + +export default function BackNavigation() { + return ( + + User 6 + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx new file mode 100644 index 000000000000..5e405e8c4e40 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx @@ -0,0 +1,15 @@ +export default function ClientErrorPage() { + return ( +
+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx new file mode 100644 index 000000000000..b22607667e7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx @@ -0,0 +1,64 @@ +import * as Sentry from '@sentry/solidstart'; +import type { ParentProps } from 'solid-js'; +import { ErrorBoundary, createSignal, onMount } from 'solid-js'; + +const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary); + +const [count, setCount] = createSignal(1); +const [caughtError, setCaughtError] = createSignal(false); + +export default function ErrorBoundaryTestPage() { + return ( + + {caughtError() && ( + + )} +
+
+ +
+
+
+ ); +} + +function Throw(props: { error: string }) { + onMount(() => { + throw new Error(props.error); + }); + return null; +} + +function SampleErrorBoundary(props: ParentProps) { + return ( + ( +
+

Error Boundary Fallback

+
+ {error.message} +
+ +
+ )} + > + {props.children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx new file mode 100644 index 000000000000..9a0b22cc38c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx @@ -0,0 +1,31 @@ +import { A } from '@solidjs/router'; + +export default function Home() { + return ( + <> +

Welcome to Solid Start

+

+ Visit docs.solidjs.com/solid-start to read the documentation +

+ + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx new file mode 100644 index 000000000000..05dce5e10a56 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx @@ -0,0 +1,17 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + throw new Error('Error thrown from Solid Start E2E test app server route'); + + return { prefecture: 'Kanagawa' }; + }); +}; + +export default function ServerErrorPage() { + const data = createAsync(() => getPrefecture()); + + return
Prefecture: {data()?.prefecture}
; +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx new file mode 100644 index 000000000000..22abd3ba8803 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx @@ -0,0 +1,21 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync, useParams } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + return { prefecture: 'Ehime' }; + }); +}; +export default function User() { + const params = useParams(); + const userData = createAsync(() => getPrefecture()); + + return ( +
+ User ID: {params.id} +
+ Prefecture: {userData()?.prefecture} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs similarity index 69% rename from dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs index a521d4f7d4fc..46cc8824da18 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'node-nestjs-basic', + proxyServerName: 'solidstart-top-level-import', }); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts new file mode 100644 index 000000000000..49f50f882b50 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts @@ -0,0 +1,90 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('captures an exception', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + await page.locator('#caughtErrorBtn').click(); + const errorEvent = await errorEventPromise; + + expect(errorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); + +test('captures a second exception after resetting the boundary', async ({ page }) => { + const firstErrorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + await page.locator('#caughtErrorBtn').click(); + const firstErrorEvent = await firstErrorEventPromise; + + expect(firstErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); + + const secondErrorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.locator('#errorBoundaryResetBtn').click(); + await page.locator('#caughtErrorBtn').click(); + const secondErrorEvent = await secondErrorEventPromise; + + expect(secondErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts new file mode 100644 index 000000000000..9e4a0269eee4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', () => { + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError('solidstart-top-level-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Uncaught error thrown from Solid Start E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Uncaught error thrown from Solid Start E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + transaction: '/client-error', + }); + expect(error.transaction).toEqual('/client-error'); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts new file mode 100644 index 000000000000..682dd34e10f9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', () => { + test('captures server action error', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Solid Start E2E test app server route'; + }); + + await page.goto(`/server-error`); + + const error = await errorEventPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Solid Start E2E test app server route', + mechanism: { + type: 'solidstart', + handled: false, + }, + }, + ], + }, + // transaction: 'GET /server-error', --> only possible with `--import` CLI flag + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts new file mode 100644 index 000000000000..bd5dece39b33 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === '/' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + const pageloadTransaction = await transactionPromise; + + expect(pageloadTransaction).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + }, + }, + transaction: '/', + transaction_info: { + source: 'url', + }, + }); +}); + +test('sends a navigation transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/5' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await page.locator('#navLink').click(); + const navigationTransaction = await transactionPromise; + + expect(navigationTransaction).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/5', + transaction_info: { + source: 'url', + }, + }); +}); + +test('updates the transaction when using the back button', async ({ page }) => { + // Solid Router sends a `-1` navigation when using the back button. + // The sentry solidRouterBrowserTracingIntegration tries to update such + // transactions with the proper name once the `useLocation` hook triggers. + const navigationTxnPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/6' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/back-navigation`); + await page.locator('#navLink').click(); + const navigationTxn = await navigationTxnPromise; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/6', + transaction_info: { + source: 'url', + }, + }); + + const backNavigationTxnPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return ( + transactionEvent?.transaction === '/back-navigation' && transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + await page.goBack(); + const backNavigationTxn = await backNavigationTxnPromise; + + expect(backNavigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/back-navigation', + transaction_info: { + source: 'url', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts new file mode 100644 index 000000000000..8072a7e75181 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', transactionEvent => { + return transactionEvent?.transaction === 'GET /users/6'; + }); + + await page.goto('/users/6'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); + +test('sends a server action transaction on client navigation', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', transactionEvent => { + return transactionEvent?.transaction === 'POST getPrefecture'; + }); + + await page.goto('/'); + await page.locator('#navLink').click(); + await page.waitForURL('/users/5'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json new file mode 100644 index 000000000000..6f11292cc5d8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vinxi/types/client", "vitest/globals", "@testing-library/jest-dom"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts new file mode 100644 index 000000000000..6c2b639dc300 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts @@ -0,0 +1,10 @@ +import solid from 'vite-plugin-solid'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [solid()], + resolve: { + conditions: ['development', 'browser'], + }, + envPrefix: 'PUBLIC_', +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart/app.config.ts index 0b9a5553fb0a..71061cf25d96 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/app.config.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart/app.config.ts @@ -1,8 +1,8 @@ -import { sentrySolidStartVite } from '@sentry/solidstart'; +import { withSentry } from '@sentry/solidstart'; import { defineConfig } from '@solidjs/start/config'; -export default defineConfig({ - vite: { - plugins: [sentrySolidStartVite()], - }, -}); +export default defineConfig( + withSentry({ + middleware: './src/middleware.ts', + }), +); diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json index 032a4af9058a..f4059823617a 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json @@ -3,9 +3,9 @@ "version": "0.0.0", "scripts": { "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", - "dev": "NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi dev", - "build": "vinxi build && sh ./post_build.sh", - "preview": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start", + "build": "vinxi build && sh post_build.sh", + "preview": "HOST=localhost PORT=3030 vinxi start", + "start:import": "HOST=localhost PORT=3030 node --import ./.output/server/instrument.server.mjs .output/server/index.mjs", "test:prod": "TEST_ENV=production playwright test", "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" @@ -27,7 +27,7 @@ "solid-js": "1.8.17", "typescript": "^5.4.5", "vinxi": "^0.4.0", - "vite": "^5.4.10", + "vite": "^5.4.11", "vite-plugin-solid": "^2.10.2", "vitest": "^1.5.0" }, diff --git a/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs index 395acfc282f9..ee2ee42980b8 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs @@ -1,7 +1,7 @@ import { getPlaywrightConfig } from '@sentry-internal/test-utils'; const config = getPlaywrightConfig({ - startCommand: 'pnpm preview', + startCommand: 'pnpm start:import', port: 3030, }); diff --git a/dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.ts new file mode 100644 index 000000000000..3dd5d8933b7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/solidstart'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server + debug: !!process.env.DEBUG, +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart/src/middleware.ts b/dev-packages/e2e-tests/test-applications/solidstart/src/middleware.ts new file mode 100644 index 000000000000..88123a035fb6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart/src/middleware.ts @@ -0,0 +1,6 @@ +import { sentryBeforeResponseMiddleware } from '@sentry/solidstart'; +import { createMiddleware } from '@solidjs/start/middleware'; + +export default createMiddleware({ + onBeforeResponse: [sentryBeforeResponseMiddleware()], +}); diff --git a/dev-packages/e2e-tests/test-applications/svelte-5/package.json b/dev-packages/e2e-tests/test-applications/svelte-5/package.json index 1022247cc6ea..ed6cf3ada0d7 100644 --- a/dev-packages/e2e-tests/test-applications/svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/svelte-5/package.json @@ -22,7 +22,7 @@ "svelte-check": "^3.6.7", "tslib": "^2.6.2", "typescript": "^5.2.2", - "vite": "^5.4.10" + "vite": "^5.4.11" }, "dependencies": { "@sentry/svelte": "latest || *" diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json index 1ce9273bba52..88d9a37ab98c 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json @@ -29,7 +29,7 @@ "svelte-check": "^3.6.0", "tslib": "^2.4.1", "typescript": "^5.0.0", - "vite": "^5.4.10" + "vite": "^5.4.11" }, "type": "module" } diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json index 0c531cd72357..5a2d9ce7b4d5 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json @@ -28,7 +28,7 @@ "svelte-check": "^3.6.0", "tslib": "^2.4.1", "typescript": "^5.0.0", - "vite": "^5.4.10" + "vite": "^5.4.11" }, "type": "module" } diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index 39f47c873a5f..3f2f87500e25 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -28,7 +28,7 @@ "svelte": "^4.2.8", "svelte-check": "^3.6.0", "typescript": "^5.0.0", - "vite": "^5.4.10" + "vite": "^5.4.11" }, "type": "module" } diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/.gitignore b/dev-packages/e2e-tests/test-applications/sveltekit/.gitignore deleted file mode 100644 index 6635cf554275..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit/.npmrc deleted file mode 100644 index 070f80f05092..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/.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/sveltekit/README.md b/dev-packages/e2e-tests/test-applications/sveltekit/README.md deleted file mode 100644 index 7c0d9fbb26ab..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# create-svelte - -Everything you need to build a Svelte project, powered by -[`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```bash -# create a new project in the current directory -npm create svelte@latest - -# create a new project in my-app -npm create svelte@latest my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a -development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target -> environment. diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/package.json b/dev-packages/e2e-tests/test-applications/sveltekit/package.json deleted file mode 100644 index 369e1715adcb..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "sveltekit", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "clean": "npx rimraf node_modules pnpm-lock.yaml", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "test:prod": "TEST_ENV=production playwright test", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test:prod" - }, - "dependencies": { - "@sentry/sveltekit": "latest || *" - }, - "devDependencies": { - "@playwright/test": "^1.44.1", - "@sentry-internal/test-utils": "link:../../../test-utils", - "@sentry/core": "latest || *", - "@sveltejs/adapter-auto": "^2.0.0", - "@sveltejs/adapter-node": "^1.2.4", - "@sveltejs/kit": "1.20.5", - "svelte": "^3.54.0", - "svelte-check": "^3.0.1", - "typescript": "^5.0.0", - "vite": "^4.5.2" - }, - "type": "module" -} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.mjs deleted file mode 100644 index 222c54f87389..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; - -const testEnv = process.env.TEST_ENV; - -if (!testEnv) { - throw new Error('No test env defined'); -} - -const config = getPlaywrightConfig({ - startCommand: testEnv === 'development' ? `pnpm dev --port 3030` : `pnpm preview --port 3030`, - port: 3030, -}); - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html deleted file mode 100644 index 435cf39f2268..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.client.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.client.ts deleted file mode 100644 index b174e9671b8d..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.client.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { env } from '$env/dynamic/public'; -import * as Sentry from '@sentry/sveltekit'; - -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: env.PUBLIC_E2E_TEST_DSN, - debug: !!env.PUBLIC_DEBUG, - tunnel: `http://localhost:3031/`, // proxy server - tracesSampleRate: 1.0, -}); - -const myErrorHandler = ({ error, event }: any) => { - console.error('An error occurred on the client side:', error, event); -}; - -export const handleError = Sentry.handleErrorWithSentry(myErrorHandler); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts deleted file mode 100644 index aca7e1b75139..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { env } from '$env/dynamic/private'; -import * as Sentry from '@sentry/sveltekit'; - -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: env.E2E_TEST_DSN, - debug: !!process.env.DEBUG, - tunnel: `http://localhost:3031/`, // proxy server - tracesSampleRate: 1.0, -}); - -// not logging anything to console to avoid noise in the test output -const myErrorHandler = ({ error, event }: any) => {}; - -export const handleError = Sentry.handleErrorWithSentry(myErrorHandler); - -export const handle = Sentry.sentryHandle(); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+layout.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+layout.svelte deleted file mode 100644 index 8b7db6f720bf..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+layout.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte deleted file mode 100644 index 31f6cb802950..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte +++ /dev/null @@ -1,32 +0,0 @@ -

Welcome to SvelteKit

-

Visit kit.svelte.dev to read the documentation

- - diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/api/users/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/api/users/+server.ts deleted file mode 100644 index d0e4371c594b..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/api/users/+server.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const GET = () => { - return new Response(JSON.stringify({ users: ['alice', 'bob', 'carol'] })); -}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/building/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/building/+page.server.ts deleted file mode 100644 index b07376ba97c9..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/building/+page.server.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { PageServerLoad } from './$types'; - -export const load = (async _event => { - return { name: 'building (server)' }; -}) satisfies PageServerLoad; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/building/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/building/+page.svelte deleted file mode 100644 index fde274c60705..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/building/+page.svelte +++ /dev/null @@ -1,6 +0,0 @@ -

Check Build

- -

- This route only exists to check that Typescript definitions - and auto instrumentation are working when the project is built. -

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/building/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/building/+page.ts deleted file mode 100644 index 049acdc1fafa..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/building/+page.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { PageLoad } from './$types'; - -export const load = (async _event => { - return { name: 'building' }; -}) satisfies PageLoad; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/client-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/client-error/+page.svelte deleted file mode 100644 index ba6b464e9324..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/client-error/+page.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - -

Client error

- - diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/+page.svelte deleted file mode 100644 index eff3fa3f2e8d..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/+page.svelte +++ /dev/null @@ -1,15 +0,0 @@ - -

Demonstrating Component Tracking

- - - - diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/Component1.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/Component1.svelte deleted file mode 100644 index a675711e4b68..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/Component1.svelte +++ /dev/null @@ -1,10 +0,0 @@ - -

Howdy, I'm component 1

- - diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/Component2.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/Component2.svelte deleted file mode 100644 index 2b2f38308077..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/Component2.svelte +++ /dev/null @@ -1,9 +0,0 @@ - -

Howdy, I'm component 2

- - diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/Component3.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/Component3.svelte deleted file mode 100644 index 9b4e028f78e7..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/components/Component3.svelte +++ /dev/null @@ -1,6 +0,0 @@ - - -

Howdy, I'm component 3

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.server.ts deleted file mode 100644 index 17dd53fb5bbb..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const load = async () => { - throw new Error('Server Load Error'); - return { - msg: 'Hello World', - }; -}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.svelte deleted file mode 100644 index 3a0942971d06..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - -

Server load error

- -

- Message: {data.msg} -

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-fetch/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-fetch/+page.server.ts deleted file mode 100644 index 709e52bcf351..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-fetch/+page.server.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const load = async ({ fetch }) => { - const res = await fetch('/api/users'); - const data = await res.json(); - return { data }; -}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-fetch/+page.svelte deleted file mode 100644 index f7f814d31b4d..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-fetch/+page.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -
-

Server Load Fetch

-

{JSON.stringify(data, null, 2)}

-
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.svelte deleted file mode 100644 index 3d682e7e3462..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - -

Server Route error

- -

- Message: {data.msg} -

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.ts deleted file mode 100644 index 298240827714..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const load = async ({ fetch }) => { - const res = await fetch('/server-route-error'); - const data = await res.json(); - return { - msg: data, - }; -}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+server.ts deleted file mode 100644 index f1a4b94b7706..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+server.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const GET = async () => { - throw new Error('Server Route Error'); - return { - msg: 'Hello World', - }; -}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.svelte deleted file mode 100644 index dc2d311a0ece..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -

Universal load error

- -

- To trigger from client: Load on another route, then navigate to this route. -

- -

- To trigger from server: Load on this route -

- -

- Message: {data.msg} -

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.ts deleted file mode 100644 index 3d72bf4a890f..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { browser } from '$app/environment'; - -export const load = async () => { - throw new Error(`Universal Load Error (${browser ? 'browser' : 'server'})`); - return { - msg: 'Hello World', - }; -}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.svelte deleted file mode 100644 index 563c51e8c850..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -

Fetching in universal load

- -

Here's a list of a few users:

- -
    - {#each data.users as user} -
  • {user}
  • - {/each} -
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.ts deleted file mode 100644 index 63c1ee68e1cb..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const load = async ({ fetch }) => { - const usersRes = await fetch('/api/users'); - const data = await usersRes.json(); - return { users: data.users }; -}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.server.ts deleted file mode 100644 index a34c5450f682..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.server.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const load = async () => { - return { - msg: 'Hi everyone!', - }; -}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.svelte deleted file mode 100644 index aa804a4518fa..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.svelte +++ /dev/null @@ -1,10 +0,0 @@ - -

- All Users: -

- -

- message: {data.msg} -

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.server.ts deleted file mode 100644 index 9388f3927018..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.server.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const load = async ({ params }) => { - return { - msg: `This is a special message for user ${params.id}`, - }; -}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.svelte deleted file mode 100644 index d348a8c57dad..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -

Route with dynamic params

- -

- User id: {$page.params.id} -

- -

- Secret message for user: {data.msg} -

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/static/favicon.png b/dev-packages/e2e-tests/test-applications/sveltekit/static/favicon.png deleted file mode 100644 index 825b9e65af7c..000000000000 Binary files a/dev-packages/e2e-tests/test-applications/sveltekit/static/favicon.png and /dev/null differ diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/svelte.config.js b/dev-packages/e2e-tests/test-applications/sveltekit/svelte.config.js deleted file mode 100644 index ba3eb7ca4745..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/svelte.config.js +++ /dev/null @@ -1,18 +0,0 @@ -import adapter from '@sveltejs/adapter-node'; -import { vitePreprocess } from '@sveltejs/kit/vite'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), - - kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter(), - }, -}; - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.client.test.ts deleted file mode 100644 index 2676a690a517..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.client.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; -import { waitForInitialPageload } from '../utils'; - -test.describe('client-side errors', () => { - test('captures error thrown on click', async ({ page }) => { - await waitForInitialPageload(page, { route: '/client-error' }); - - const errorEventPromise = waitForError('sveltekit', errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Click Error'; - }); - - await page.getByText('Throw error').click(); - - await expect(errorEventPromise).resolves.toBeDefined(); - - const errorEvent = await errorEventPromise; - - const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; - - expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( - expect.objectContaining({ - function: expect.stringContaining('HTMLButtonElement'), - lineno: 1, - in_app: true, - }), - ); - }); - - test('captures universal load error', async ({ page }) => { - await waitForInitialPageload(page); - await page.reload(); - - const errorEventPromise = waitForError('sveltekit', errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (browser)'; - }); - - // navigating triggers the error on the client - await page.getByText('Universal Load error').click(); - - const errorEvent = await errorEventPromise; - const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; - - expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( - expect.objectContaining({ - lineno: 1, - in_app: true, - }), - ); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.server.test.ts deleted file mode 100644 index fbf8cf6e673a..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.server.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; - -test.describe('server-side errors', () => { - test('captures universal load error', async ({ page }) => { - const errorEventPromise = waitForError('sveltekit', errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (server)'; - }); - - await page.goto('/universal-load-error'); - - const errorEvent = await errorEventPromise; - const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; - - expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( - expect.objectContaining({ - function: 'load$1', - lineno: 3, - in_app: true, - }), - ); - - expect(errorEvent.request).toEqual({ - cookies: {}, - headers: expect.objectContaining({ - accept: expect.any(String), - 'user-agent': expect.any(String), - }), - method: 'GET', - url: 'http://localhost:3030/universal-load-error', - }); - }); - - test('captures server load error', async ({ page }) => { - const errorEventPromise = waitForError('sveltekit', errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Server Load Error'; - }); - - await page.goto('/server-load-error'); - - const errorEvent = await errorEventPromise; - const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; - - expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( - expect.objectContaining({ - function: 'load$1', - lineno: 3, - in_app: true, - }), - ); - - expect(errorEvent.request).toEqual({ - cookies: {}, - headers: expect.objectContaining({ - accept: expect.any(String), - 'user-agent': expect.any(String), - }), - method: 'GET', - url: 'http://localhost:3030/server-load-error', - }); - }); - - test('captures server route (GET) error', async ({ page }) => { - const errorEventPromise = waitForError('sveltekit', errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Server Route Error'; - }); - - await page.goto('/server-route-error'); - - const errorEvent = await errorEventPromise; - const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; - - expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( - expect.objectContaining({ - filename: 'app:///_server.ts.js', - function: 'GET', - lineno: 2, - in_app: true, - }), - ); - - expect(errorEvent.transaction).toEqual('GET /server-route-error'); - - expect(errorEvent.request).toEqual({ - cookies: {}, - headers: expect.objectContaining({ - accept: expect.any(String), - }), - method: 'GET', - url: 'http://localhost:3030/server-route-error', - }); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.client.test.ts deleted file mode 100644 index 33515a950d3c..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.client.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { waitForInitialPageload } from '../utils.js'; - -test('records manually added component tracking spans', async ({ page }) => { - const componentTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { - return txnEvent?.transaction === '/components'; - }); - - await waitForInitialPageload(page); - - await page.getByText('Component Tracking').click(); - - const componentTxnEvent = await componentTxnEventPromise; - - expect(componentTxnEvent.spans).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.init', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.init', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.init', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.init', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), - ]), - ); -}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.server.test.ts deleted file mode 100644 index 5c3fd61e5467..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.server.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('server pageload request span has nested request span for sub request', async ({ page }) => { - const serverTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { - return txnEvent?.transaction === 'GET /server-load-fetch'; - }); - - await page.goto('/server-load-fetch'); - - const serverTxnEvent = await serverTxnEventPromise; - const spans = serverTxnEvent.spans; - - expect(serverTxnEvent).toMatchObject({ - transaction: 'GET /server-load-fetch', - transaction_info: { source: 'route' }, - type: 'transaction', - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.sveltekit', - }, - }, - }); - - expect(spans).toEqual( - expect.arrayContaining([ - // load span where the server load function initiates the sub request: - expect.objectContaining({ op: 'function.sveltekit.server.load', description: '/server-load-fetch' }), - // sub request span: - expect.objectContaining({ op: 'http.server', description: 'GET /api/users' }), - ]), - ); - - expect(serverTxnEvent.request).toEqual({ - cookies: {}, - headers: expect.objectContaining({ - accept: expect.any(String), - 'user-agent': expect.any(String), - }), - method: 'GET', - url: 'http://localhost:3030/server-load-fetch', - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.test.ts deleted file mode 100644 index c452e1d48cb3..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { waitForInitialPageload } from '../utils.js'; - -test('sends a pageload transaction', async ({ page }) => { - const pageloadTransactionEventPromise = waitForTransaction('sveltekit', (transactionEvent: any) => { - return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; - }); - - await page.goto('/'); - - const transactionEvent = await pageloadTransactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.sveltekit', - }, - }, - }); -}); - -test('captures a distributed pageload trace', async ({ page }) => { - const clientTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { - return txnEvent?.transaction === '/users/[id]'; - }); - - const serverTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { - return txnEvent?.transaction === 'GET /users/[id]'; - }); - - await page.goto('/users/123xyz'); - - const [clientTxnEvent, serverTxnEvent] = await Promise.all([clientTxnEventPromise, serverTxnEventPromise]); - - expect(clientTxnEvent).toMatchObject({ - transaction: '/users/[id]', - transaction_info: { source: 'route' }, - type: 'transaction', - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.sveltekit', - }, - }, - }); - - expect(serverTxnEvent).toMatchObject({ - transaction: 'GET /users/[id]', - transaction_info: { source: 'route' }, - type: 'transaction', - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.sveltekit', - }, - }, - }); - // connected trace - expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); - - // weird but server txn is parent of client txn - expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); -}); - -test('captures a distributed navigation trace', async ({ page }) => { - const clientNavigationTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { - return txnEvent?.transaction === '/users/[id]'; - }); - - const serverTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { - return txnEvent?.transaction === 'GET /users/[id]'; - }); - - await waitForInitialPageload(page); - - // navigation to page - const clickPromise = page.getByText('Route with Params').click(); - - const [clientTxnEvent, serverTxnEvent, _1] = await Promise.all([ - clientNavigationTxnEventPromise, - serverTxnEventPromise, - clickPromise, - ]); - - expect(clientTxnEvent).toMatchObject({ - transaction: '/users/[id]', - transaction_info: { source: 'route' }, - type: 'transaction', - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.sveltekit', - }, - }, - }); - - expect(serverTxnEvent).toMatchObject({ - transaction: 'GET /users/[id]', - transaction_info: { source: 'route' }, - type: 'transaction', - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.sveltekit', - }, - }, - }); - - // trace is connected - expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); -}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/tsconfig.json b/dev-packages/e2e-tests/test-applications/sveltekit/tsconfig.json deleted file mode 100644 index 115dd34bec96..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "allowImportingTsExtensions": true - } - // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in -} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts deleted file mode 100644 index 320d41aba389..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Page } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -/** - * Helper function that waits for the initial pageload to complete. - * - * This function - * - loads the given route ("/" by default) - * - waits for SvelteKit's hydration - * - waits for the pageload transaction to be sent (doesn't assert on it though) - * - * Useful for tests that test outcomes of _navigations_ after an initial pageload. - * Waiting on the pageload transaction excludes edge cases where navigations occur - * so quickly that the pageload idle transaction is still active. This might lead - * to cases where the routing span would be attached to the pageload transaction - * and hence eliminates a lot of flakiness. - * - */ -export async function waitForInitialPageload( - page: Page, - opts?: { route?: string; parameterizedRoute?: string; debug?: boolean }, -) { - const route = opts?.route ?? '/'; - const txnName = opts?.parameterizedRoute ?? route; - const debug = opts?.debug ?? false; - - const clientPageloadTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { - debug && - console.log({ - txn: txnEvent?.transaction, - op: txnEvent.contexts?.trace?.op, - trace: txnEvent.contexts?.trace?.trace_id, - span: txnEvent.contexts?.trace?.span_id, - parent: txnEvent.contexts?.trace?.parent_span_id, - }); - - return txnEvent?.transaction === txnName && txnEvent.contexts?.trace?.op === 'pageload'; - }); - - await Promise.all([ - page.goto(route), - // the test app adds the "hydrated" class to the body when hydrating - page.waitForSelector('body.hydrated'), - // also waiting for the initial pageload txn so that later navigations don't interfere - clientPageloadTxnEventPromise, - ]); - - // let's add a buffer because it seems like the hydrated flag isn't enough :( - // guess: The layout finishes hydration/mounting before the components within finish - // await page.waitForTimeout(10_000); - - debug && console.log('hydrated'); -} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/vite.config.js b/dev-packages/e2e-tests/test-applications/sveltekit/vite.config.js deleted file mode 100644 index 1a410bee7e11..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/vite.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import { sentrySvelteKit } from '@sentry/sveltekit'; -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [ - sentrySvelteKit({ - autoUploadSourceMaps: false, - }), - sveltekit(), - ], -}); diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json index 54387ae46cde..96a26ee98447 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@sentry/react": "latest || *", - "@tanstack/react-router": "1.34.5", + "@tanstack/react-router": "1.64.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -24,7 +24,7 @@ "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^3.5.0", "typescript": "^5.2.2", - "vite": "^5.4.10", + "vite": "^5.4.11", "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils" }, diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock b/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock deleted file mode 100644 index 84f247232b85..000000000000 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock +++ /dev/null @@ -1,940 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@esbuild/aix-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" - integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== - -"@esbuild/android-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" - integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== - -"@esbuild/android-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" - integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== - -"@esbuild/android-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" - integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== - -"@esbuild/darwin-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" - integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== - -"@esbuild/darwin-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" - integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== - -"@esbuild/freebsd-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" - integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== - -"@esbuild/freebsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" - integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== - -"@esbuild/linux-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" - integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== - -"@esbuild/linux-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" - integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== - -"@esbuild/linux-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" - integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== - -"@esbuild/linux-loong64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" - integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== - -"@esbuild/linux-mips64el@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" - integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== - -"@esbuild/linux-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" - integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== - -"@esbuild/linux-riscv64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" - integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== - -"@esbuild/linux-s390x@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" - integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== - -"@esbuild/linux-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" - integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== - -"@esbuild/netbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" - integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== - -"@esbuild/openbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" - integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== - -"@esbuild/sunos-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" - integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== - -"@esbuild/win32-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" - integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== - -"@esbuild/win32-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" - integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== - -"@esbuild/win32-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" - integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== - -"@eslint-community/eslint-utils@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== - dependencies: - eslint-visitor-keys "^3.3.0" - -"@eslint-community/regexpp@^4.10.0": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" - integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@playwright/test@^1.44.1": - version "1.46.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.46.1.tgz#a8dfdcd623c4c23bb1b7ea588058aad41055c188" - integrity sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA== - dependencies: - playwright "1.46.1" - -"@rollup/rollup-android-arm-eabi@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz#7f4c4d8cd5ccab6e95d6750dbe00321c1f30791e" - integrity sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ== - -"@rollup/rollup-android-arm64@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz#17ea71695fb1518c2c324badbe431a0bd1879f2d" - integrity sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA== - -"@rollup/rollup-darwin-arm64@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz#dac0f0d0cfa73e7d5225ae6d303c13c8979e7999" - integrity sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ== - -"@rollup/rollup-darwin-x64@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz#8f63baa1d31784904a380d2e293fa1ddf53dd4a2" - integrity sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ== - -"@rollup/rollup-freebsd-arm64@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz#30ed247e0df6e8858cdc6ae4090e12dbeb8ce946" - integrity sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA== - -"@rollup/rollup-freebsd-x64@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz#57846f382fddbb508412ae07855b8a04c8f56282" - integrity sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ== - -"@rollup/rollup-linux-arm-gnueabihf@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz#378ca666c9dae5e6f94d1d351e7497c176e9b6df" - integrity sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA== - -"@rollup/rollup-linux-arm-musleabihf@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz#a692eff3bab330d5c33a5d5813a090c15374cddb" - integrity sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg== - -"@rollup/rollup-linux-arm64-gnu@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz#6b1719b76088da5ac1ae1feccf48c5926b9e3db9" - integrity sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA== - -"@rollup/rollup-linux-arm64-musl@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz#865baf5b6f5ff67acb32e5a359508828e8dc5788" - integrity sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A== - -"@rollup/rollup-linux-loongarch64-gnu@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz#23c6609ba0f7fa7a7f2038b6b6a08555a5055a87" - integrity sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA== - -"@rollup/rollup-linux-powerpc64le-gnu@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz#652ef0d9334a9f25b9daf85731242801cb0fc41c" - integrity sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A== - -"@rollup/rollup-linux-riscv64-gnu@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz#1eb6651839ee6ebca64d6cc64febbd299e95e6bd" - integrity sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA== - -"@rollup/rollup-linux-s390x-gnu@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz#015c52293afb3ff2a293cf0936b1d43975c1e9cd" - integrity sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg== - -"@rollup/rollup-linux-x64-gnu@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz#b83001b5abed2bcb5e2dbeec6a7e69b194235c1e" - integrity sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw== - -"@rollup/rollup-linux-x64-musl@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz#6cc7c84cd4563737f8593e66f33b57d8e228805b" - integrity sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g== - -"@rollup/rollup-win32-arm64-msvc@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz#631ffeee094d71279fcd1fe8072bdcf25311bc11" - integrity sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A== - -"@rollup/rollup-win32-ia32-msvc@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz#06d1d60d5b9f718e8a6c4a43f82e3f9e3254587f" - integrity sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA== - -"@rollup/rollup-win32-x64-msvc@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz#4dff5c4259ebe6c5b4a8f2c5bc3829b7a8447ff0" - integrity sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA== - -"@sentry-internal/browser-utils@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.4.0.tgz#5b108878e93713757d75e7e8ae7780297d36ad17" - integrity sha512-Mfm3TK3KUlghhuKM3rjTeD4D5kAiB7iVNFoaDJIJBVKa67M9BvlNTnNJMDi7+9rV4RuLQYxXn0p5HEZJFYp3Zw== - dependencies: - "@sentry/core" "8.4.0" - "@sentry/types" "8.4.0" - "@sentry/utils" "8.4.0" - -"@sentry-internal/feedback@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.4.0.tgz#81067dadda249b354b72f5adba20374dea43fdf4" - integrity sha512-1/WshI2X9seZAQXrOiv6/LU08fbSSvJU0b1ZWMhn+onb/FWPomsL/UN0WufCYA65S5JZGdaWC8fUcJxWC8PATQ== - dependencies: - "@sentry/core" "8.4.0" - "@sentry/types" "8.4.0" - "@sentry/utils" "8.4.0" - -"@sentry-internal/replay-canvas@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.4.0.tgz#cf5e903d8935ba6b60a5027d0055902987353920" - integrity sha512-g+U4IPQdODCg7fQQVNvH6ix05Tl1mOQXXRexgtp+tXdys4sHQSBUYraJYZy+mY3OGnLRgKFqELM0fnffJSpuyQ== - dependencies: - "@sentry-internal/replay" "8.4.0" - "@sentry/core" "8.4.0" - "@sentry/types" "8.4.0" - "@sentry/utils" "8.4.0" - -"@sentry-internal/replay@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.4.0.tgz#8fc4a6bf1d5f480fcde2d56cd75042953e44efda" - integrity sha512-RSzQwCF/QTi5/5XAuj0VJImAhu4MheeHYvAbr/PuMSF4o1j89gBA7e3boA4u8633IqUeu5w3S5sb6jVrKaVifg== - dependencies: - "@sentry-internal/browser-utils" "8.4.0" - "@sentry/core" "8.4.0" - "@sentry/types" "8.4.0" - "@sentry/utils" "8.4.0" - -"@sentry-internal/test-utils@link:../../../test-utils": - version "8.43.0" - -"@sentry/browser@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.4.0.tgz#f4aa381eab212432d71366884693a36c2e3a1675" - integrity sha512-hmXeIZBdN0A6yCuoMTcigGxLl42nbeb205fXtouwE7Maa0qM2HM+Ijq0sHzbhxR3zU0JXDtcJh1k6wtJOREJ3g== - dependencies: - "@sentry-internal/browser-utils" "8.4.0" - "@sentry-internal/feedback" "8.4.0" - "@sentry-internal/replay" "8.4.0" - "@sentry-internal/replay-canvas" "8.4.0" - "@sentry/core" "8.4.0" - "@sentry/types" "8.4.0" - "@sentry/utils" "8.4.0" - -"@sentry/core@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.4.0.tgz#ab3f7202f3cae82daf4c3c408f50d2c6fb913620" - integrity sha512-0eACPlJvKloFIlcT1c/vjGnvqxLxpGyGuSsU7uonrkmBqIRwLYXWtR4PoHapysKtjPVoHAn9au50ut6ymC2V8Q== - dependencies: - "@sentry/types" "8.4.0" - "@sentry/utils" "8.4.0" - -"@sentry/react@latest || *": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.4.0.tgz#95f4fed03709b231770a4f32d3c960c544b0dc3c" - integrity sha512-YnDN+szKFm1fQ9311nAulsRbboeMbqNmosMLA6PweBDEwD0HEJsovQT+ZJxXiOL220qsgWVJzk+aTPtf+oY4wA== - dependencies: - "@sentry/browser" "8.4.0" - "@sentry/core" "8.4.0" - "@sentry/types" "8.4.0" - "@sentry/utils" "8.4.0" - hoist-non-react-statics "^3.3.2" - -"@sentry/types@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.4.0.tgz#42500005a198ff8c247490434ed55e0a9f975ad1" - integrity sha512-mHUaaYEQCNukzYsTLp4rP2NNO17vUf+oSGS6qmhrsGqmGNICKw2CIwJlPPGeAkq9Y4tiUOye2m5OT1xsOtxLIw== - -"@sentry/utils@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.4.0.tgz#1b816e65d8dbf055c5e1554361aaf9a8a8a94102" - integrity sha512-oDF0RVWW0AyEnsP1x4McHUvQSAxJgx3G6wM9Sb4wc1F8rwsHnCtGHc+WRZ5Gd2AXC5EGkfbg5919+1ku/L4Dww== - dependencies: - "@sentry/types" "8.4.0" - -"@swc/core-darwin-arm64@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.7.tgz#2b5cdbd34e4162e50de6147dd1a5cb12d23b08e8" - integrity sha512-bZLVHPTpH3h6yhwVl395k0Mtx8v6CGhq5r4KQdAoPbADU974Mauz1b6ViHAJ74O0IVE5vyy7tD3OpkQxL/vMDQ== - -"@swc/core-darwin-x64@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.5.7.tgz#6aa7e3c01ab8e5e41597f8a24ff24c4e50936a46" - integrity sha512-RpUyu2GsviwTc2qVajPL0l8nf2vKj5wzO3WkLSHAHEJbiUZk83NJrZd1RVbEknIMO7+Uyjh54hEh8R26jSByaw== - -"@swc/core-linux-arm-gnueabihf@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.7.tgz#160108633b9e1d1ad05f815bedc7e9eb5d59fc2a" - integrity sha512-cTZWTnCXLABOuvWiv6nQQM0hP6ZWEkzdgDvztgHI/+u/MvtzJBN5lBQ2lue/9sSFYLMqzqff5EHKlFtrJCA9dQ== - -"@swc/core-linux-arm64-gnu@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.7.tgz#cbfa512683c73227ad25552f3b3e722b0e7fbd1d" - integrity sha512-hoeTJFBiE/IJP30Be7djWF8Q5KVgkbDtjySmvYLg9P94bHg9TJPSQoC72tXx/oXOgXvElDe/GMybru0UxhKx4g== - -"@swc/core-linux-arm64-musl@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.7.tgz#80239cb58fe57f3c86b44617fe784530ec55ee2b" - integrity sha512-+NDhK+IFTiVK1/o7EXdCeF2hEzCiaRSrb9zD7X2Z7inwWlxAntcSuzZW7Y6BRqGQH89KA91qYgwbnjgTQ22PiQ== - -"@swc/core-linux-x64-gnu@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.7.tgz#a699c1632de60b6a63b7fdb7abcb4fef317e57ca" - integrity sha512-25GXpJmeFxKB+7pbY7YQLhWWjkYlR+kHz5I3j9WRl3Lp4v4UD67OGXwPe+DIcHqcouA1fhLhsgHJWtsaNOMBNg== - -"@swc/core-linux-x64-musl@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.7.tgz#8e4c203d6bc41e7f85d7d34d0fdf4ef751fa626c" - integrity sha512-0VN9Y5EAPBESmSPPsCJzplZHV26akC0sIgd3Hc/7S/1GkSMoeuVL+V9vt+F/cCuzr4VidzSkqftdP3qEIsXSpg== - -"@swc/core-win32-arm64-msvc@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.7.tgz#31e3d42b8c0aa79f0ea1a980c0dd1a999d378ed7" - integrity sha512-RtoNnstBwy5VloNCvmvYNApkTmuCe4sNcoYWpmY7C1+bPR+6SOo8im1G6/FpNem8AR5fcZCmXHWQ+EUmRWJyuA== - -"@swc/core-win32-ia32-msvc@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.7.tgz#a235285f9f62850aefcf9abb03420f2c54f63638" - integrity sha512-Xm0TfvcmmspvQg1s4+USL3x8D+YPAfX2JHygvxAnCJ0EHun8cm2zvfNBcsTlnwYb0ybFWXXY129aq1wgFC9TpQ== - -"@swc/core-win32-x64-msvc@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.7.tgz#f84641393b5223450d00d97bfff877b8b69d7c9b" - integrity sha512-tp43WfJLCsKLQKBmjmY/0vv1slVywR5Q4qKjF5OIY8QijaEW7/8VwPyUyVoJZEnDgv9jKtUTG5PzqtIYPZGnyg== - -"@swc/core@^1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.5.7.tgz#e1db7b9887d5f34eb4a3256a738d0c5f1b018c33" - integrity sha512-U4qJRBefIJNJDRCCiVtkfa/hpiZ7w0R6kASea+/KLp+vkus3zcLSB8Ub8SvKgTIxjWpwsKcZlPf5nrv4ls46SQ== - dependencies: - "@swc/counter" "^0.1.2" - "@swc/types" "0.1.7" - optionalDependencies: - "@swc/core-darwin-arm64" "1.5.7" - "@swc/core-darwin-x64" "1.5.7" - "@swc/core-linux-arm-gnueabihf" "1.5.7" - "@swc/core-linux-arm64-gnu" "1.5.7" - "@swc/core-linux-arm64-musl" "1.5.7" - "@swc/core-linux-x64-gnu" "1.5.7" - "@swc/core-linux-x64-musl" "1.5.7" - "@swc/core-win32-arm64-msvc" "1.5.7" - "@swc/core-win32-ia32-msvc" "1.5.7" - "@swc/core-win32-x64-msvc" "1.5.7" - -"@swc/counter@^0.1.2", "@swc/counter@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" - integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== - -"@swc/types@0.1.7": - version "0.1.7" - resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.7.tgz#ea5d658cf460abff51507ca8d26e2d391bafb15e" - integrity sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ== - dependencies: - "@swc/counter" "^0.1.3" - -"@tanstack/history@1.31.16": - version "1.31.16" - resolved "https://registry.yarnpkg.com/@tanstack/history/-/history-1.31.16.tgz#6b4947e967af3173ce4929d54d9cb97234646e32" - integrity sha512-rahAZXlR879P7dngDH7BZwGYiODA9D5Hqo6nUHn9GAURcqZU5IW0ZiT54dPtV5EPES7muZZmknReYueDHs7FFQ== - -"@tanstack/react-router@1.34.5": - version "1.34.5" - resolved "https://registry.yarnpkg.com/@tanstack/react-router/-/react-router-1.34.5.tgz#2c5bc5cd6b246f830ce586c51a87f95352481957" - integrity sha512-mOMbNHSJ1cAgRuJj9W35wteQL7zFiCNJYgg3QHkxj+obO9zQWiAwycFs0hQTRxqzGfC+jhVLJe1+cW93BhqKyA== - dependencies: - "@tanstack/history" "1.31.16" - "@tanstack/react-store" "^0.2.1" - tiny-invariant "^1.3.1" - tiny-warning "^1.0.3" - -"@tanstack/react-store@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-store/-/react-store-0.2.1.tgz#c1a04c85d403d842e56c6d0709211f013bdd1021" - integrity sha512-tEbMCQjbeVw9KOP/202LfqZMSNAVi6zYkkp1kBom8nFuMx/965Hzes3+6G6b/comCwVxoJU8Gg9IrcF8yRPthw== - dependencies: - "@tanstack/store" "0.1.3" - use-sync-external-store "^1.2.0" - -"@tanstack/store@0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@tanstack/store/-/store-0.1.3.tgz#b8410435dac0a0f6d3fe77d49509f296905d4c73" - integrity sha512-GnolmC8Fr4mvsHE1fGQmR3Nm0eBO3KnZjDU0a+P3TeQNM/dDscFGxtA7p31NplQNW3KwBw4t1RVFmz0VeKLxcw== - -"@types/estree@1.0.6": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" - integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== - -"@types/prop-types@*": - version "15.7.12" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" - integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== - -"@types/react-dom@^18.2.22": - version "18.3.0" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" - integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== - dependencies: - "@types/react" "*" - -"@types/react@*", "@types/react@^18.2.66": - version "18.3.3" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f" - integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - -"@typescript-eslint/eslint-plugin@^7.2.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.10.0.tgz#07854a236f107bb45cbf4f62b89474cbea617f50" - integrity sha512-PzCr+a/KAef5ZawX7nbyNwBDtM1HdLIT53aSA2DDlxmxMngZ43O8SIePOeX8H5S+FHXeI6t97mTt/dDdzY4Fyw== - dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "7.10.0" - "@typescript-eslint/type-utils" "7.10.0" - "@typescript-eslint/utils" "7.10.0" - "@typescript-eslint/visitor-keys" "7.10.0" - graphemer "^1.4.0" - ignore "^5.3.1" - natural-compare "^1.4.0" - ts-api-utils "^1.3.0" - -"@typescript-eslint/parser@^7.2.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.10.0.tgz#e6ac1cba7bc0400a4459e7eb5b23115bd71accfb" - integrity sha512-2EjZMA0LUW5V5tGQiaa2Gys+nKdfrn2xiTIBLR4fxmPmVSvgPcKNW+AE/ln9k0A4zDUti0J/GZXMDupQoI+e1w== - dependencies: - "@typescript-eslint/scope-manager" "7.10.0" - "@typescript-eslint/types" "7.10.0" - "@typescript-eslint/typescript-estree" "7.10.0" - "@typescript-eslint/visitor-keys" "7.10.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@7.10.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz#054a27b1090199337a39cf755f83d9f2ce26546b" - integrity sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg== - dependencies: - "@typescript-eslint/types" "7.10.0" - "@typescript-eslint/visitor-keys" "7.10.0" - -"@typescript-eslint/type-utils@7.10.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.10.0.tgz#8a75accce851d0a331aa9331268ef64e9b300270" - integrity sha512-D7tS4WDkJWrVkuzgm90qYw9RdgBcrWmbbRkrLA4d7Pg3w0ttVGDsvYGV19SH8gPR5L7OtcN5J1hTtyenO9xE9g== - dependencies: - "@typescript-eslint/typescript-estree" "7.10.0" - "@typescript-eslint/utils" "7.10.0" - debug "^4.3.4" - ts-api-utils "^1.3.0" - -"@typescript-eslint/types@7.10.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.10.0.tgz#da92309c97932a3a033762fd5faa8b067de84e3b" - integrity sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg== - -"@typescript-eslint/typescript-estree@7.10.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz#6dcdc5de3149916a6a599fa89dde5c471b88b8bb" - integrity sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g== - dependencies: - "@typescript-eslint/types" "7.10.0" - "@typescript-eslint/visitor-keys" "7.10.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" - -"@typescript-eslint/utils@7.10.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.10.0.tgz#8ee43e5608c9f439524eaaea8de5b358b15c51b3" - integrity sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "7.10.0" - "@typescript-eslint/types" "7.10.0" - "@typescript-eslint/typescript-estree" "7.10.0" - -"@typescript-eslint/visitor-keys@7.10.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz#2af2e91e73a75dd6b70b4486c48ae9d38a485a78" - integrity sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg== - dependencies: - "@typescript-eslint/types" "7.10.0" - eslint-visitor-keys "^3.4.3" - -"@vitejs/plugin-react-swc@^3.5.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz#e456c0a6d7f562268e1d231af9ac46b86ef47d88" - integrity sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA== - dependencies: - "@swc/core" "^1.5.7" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -csstype@^3.0.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" - integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== - -debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -esbuild@^0.21.3: - version "0.21.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" - integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== - optionalDependencies: - "@esbuild/aix-ppc64" "0.21.5" - "@esbuild/android-arm" "0.21.5" - "@esbuild/android-arm64" "0.21.5" - "@esbuild/android-x64" "0.21.5" - "@esbuild/darwin-arm64" "0.21.5" - "@esbuild/darwin-x64" "0.21.5" - "@esbuild/freebsd-arm64" "0.21.5" - "@esbuild/freebsd-x64" "0.21.5" - "@esbuild/linux-arm" "0.21.5" - "@esbuild/linux-arm64" "0.21.5" - "@esbuild/linux-ia32" "0.21.5" - "@esbuild/linux-loong64" "0.21.5" - "@esbuild/linux-mips64el" "0.21.5" - "@esbuild/linux-ppc64" "0.21.5" - "@esbuild/linux-riscv64" "0.21.5" - "@esbuild/linux-s390x" "0.21.5" - "@esbuild/linux-x64" "0.21.5" - "@esbuild/netbsd-x64" "0.21.5" - "@esbuild/openbsd-x64" "0.21.5" - "@esbuild/sunos-x64" "0.21.5" - "@esbuild/win32-arm64" "0.21.5" - "@esbuild/win32-ia32" "0.21.5" - "@esbuild/win32-x64" "0.21.5" - -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - -fast-glob@^3.2.9: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -fsevents@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -fsevents@~2.3.2, fsevents@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - -hoist-non-react-statics@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -ignore@^5.2.0, ignore@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" - integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-glob@^4.0.1, is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -"js-tokens@^3.0.0 || ^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -loose-envify@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -minimatch@^9.0.4: - version "9.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" - integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== - dependencies: - brace-expansion "^2.0.1" - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -nanoid@^3.3.7: - version "3.3.8" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" - integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -playwright-core@1.46.1: - version "1.46.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.46.1.tgz#28f3ab35312135dda75b0c92a3e5c0e7edb9cc8b" - integrity sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A== - -playwright@1.46.1: - version "1.46.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.46.1.tgz#ea562bc48373648e10420a10c16842f0b227c218" - integrity sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng== - dependencies: - playwright-core "1.46.1" - optionalDependencies: - fsevents "2.3.2" - -postcss@^8.4.43: - version "8.4.49" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" - integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== - dependencies: - nanoid "^3.3.7" - picocolors "^1.1.1" - source-map-js "^1.2.1" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -react-dom@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" - -react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== - dependencies: - loose-envify "^1.1.0" - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rollup@^4.20.0: - version "4.28.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.1.tgz#7718ba34d62b449dfc49adbfd2f312b4fe0df4de" - integrity sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg== - dependencies: - "@types/estree" "1.0.6" - optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.28.1" - "@rollup/rollup-android-arm64" "4.28.1" - "@rollup/rollup-darwin-arm64" "4.28.1" - "@rollup/rollup-darwin-x64" "4.28.1" - "@rollup/rollup-freebsd-arm64" "4.28.1" - "@rollup/rollup-freebsd-x64" "4.28.1" - "@rollup/rollup-linux-arm-gnueabihf" "4.28.1" - "@rollup/rollup-linux-arm-musleabihf" "4.28.1" - "@rollup/rollup-linux-arm64-gnu" "4.28.1" - "@rollup/rollup-linux-arm64-musl" "4.28.1" - "@rollup/rollup-linux-loongarch64-gnu" "4.28.1" - "@rollup/rollup-linux-powerpc64le-gnu" "4.28.1" - "@rollup/rollup-linux-riscv64-gnu" "4.28.1" - "@rollup/rollup-linux-s390x-gnu" "4.28.1" - "@rollup/rollup-linux-x64-gnu" "4.28.1" - "@rollup/rollup-linux-x64-musl" "4.28.1" - "@rollup/rollup-win32-arm64-msvc" "4.28.1" - "@rollup/rollup-win32-ia32-msvc" "4.28.1" - "@rollup/rollup-win32-x64-msvc" "4.28.1" - fsevents "~2.3.2" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" - -semver@^7.6.0: - version "7.6.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" - integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== - -tiny-invariant@^1.3.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" - integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== - -tiny-warning@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -ts-api-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" - integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== - -typescript@^5.2.2: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== - -use-sync-external-store@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" - integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== - -vite@^5.4.10: - version "5.4.11" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5" - integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q== - dependencies: - esbuild "^0.21.3" - postcss "^8.4.43" - rollup "^4.20.0" - optionalDependencies: - fsevents "~2.3.3" diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json index f34bdf6d6c0e..3a2c38f43633 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/package.json +++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json @@ -25,14 +25,14 @@ "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *", "@tsconfig/node20": "^20.1.2", - "@types/node": "^20.11.10", + "@types/node": "^18.19.1", "@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue-jsx": "^3.1.0", "@vue/tsconfig": "^0.5.1", "http-server": "^14.1.1", "npm-run-all2": "^6.2.0", "typescript": "~5.3.0", - "vite": "^5.4.10", + "vite": "^5.4.11", "vue-tsc": "^1.8.27" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts index b940023b3153..4a08ed4ddbcc 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts @@ -7,7 +7,7 @@ import router from './router'; import { createPinia } from 'pinia'; import * as Sentry from '@sentry/vue'; -import { browserTracingIntegration } from '@sentry/vue'; +import { browserTracingIntegration, vueIntegration } from '@sentry/vue'; const app = createApp(App); const pinia = createPinia(); @@ -17,12 +17,16 @@ Sentry.init({ dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, tracesSampleRate: 1.0, integrations: [ + vueIntegration({ + tracingOptions: { + trackComponents: ['ComponentMainView', ''], + }, + }), browserTracingIntegration({ router, }), ], tunnel: `http://localhost:3031/`, // proxy server - trackComponents: ['ComponentMainView', ''], }); pinia.use( diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts index 262cda11b366..b86e56eb4b83 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts @@ -19,7 +19,7 @@ test('sends an error', async ({ page }) => { type: 'Error', value: 'This is a Vue test error', mechanism: { - type: 'generic', + type: 'vue', handled: false, }, }, @@ -47,7 +47,7 @@ test('sends an error with a parameterized transaction name', async ({ page }) => type: 'Error', value: 'This is a Vue test error', mechanism: { - type: 'generic', + type: 'vue', handled: false, }, }, diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs b/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs index 11874cb62374..0818243ad9ee 100644 --- a/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs +++ b/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs @@ -19,6 +19,20 @@ webpack( }, plugins: [new HtmlWebpackPlugin(), new webpack.EnvironmentPlugin(['E2E_TEST_DSN'])], mode: 'production', + // webpack 4 does not support ES2020 features out of the box, so we need to transpile them + module: { + rules: [ + { + test: /\.(?:js|mjs|cjs)$/, + use: { + loader: 'babel-loader', + options: { + presets: [['@babel/preset-env', { targets: 'ie 11' }]], + }, + }, + }, + ], + }, }, (err, stats) => { if (err) { diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/package.json b/dev-packages/e2e-tests/test-applications/webpack-4/package.json index 2195742a148a..95d3d5c39a3e 100644 --- a/dev-packages/e2e-tests/test-applications/webpack-4/package.json +++ b/dev-packages/e2e-tests/test-applications/webpack-4/package.json @@ -1,5 +1,5 @@ { - "name": "webpack-4-test", + "name": "webpack-4", "version": "1.0.0", "scripts": { "start": "serve -s build", @@ -11,6 +11,9 @@ "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/browser": "latest || *", + "babel-loader": "^8.0.0", + "@babel/core": "^7.0.0", + "@babel/preset-env": "^7.0.0", "webpack": "^4.47.0", "terser-webpack-plugin": "^4.2.3", "html-webpack-plugin": "^4.5.2", diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index 585cbbfa10a5..ee65c5218d4a 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -164,20 +164,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! - # TODO(v9): Remove '@sentry/types': access: $all publish: $all unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! - # TODO(v9): Remove - '@sentry/utils': - access: $all - publish: $all - unpublish: $all - # proxy: npmjs # Don't proxy for E2E tests! - '@sentry/vercel-edge': access: $all publish: $all diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 24be97583c1c..6680405b3ac1 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -3,7 +3,7 @@ "version": "8.45.0", "license": "MIT", "engines": { - "node": ">=14.18" + "node": ">=18" }, "private": true, "main": "build/cjs/index.js", @@ -16,7 +16,7 @@ "build:types": "tsc -p tsconfig.types.json", "clean": "rimraf -g **/node_modules && run-p clean:script", "clean:script": "node scripts/clean.js", - "prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)", + "prisma:init": "cd suites/tracing/prisma-orm && yarn && yarn setup", "lint": "eslint . --format stylish", "fix": "eslint . --format stylish --fix", "type-check": "tsc", @@ -30,7 +30,7 @@ "@nestjs/common": "10.4.6", "@nestjs/core": "10.4.6", "@nestjs/platform-express": "10.4.6", - "@prisma/client": "5.9.1", + "@prisma/client": "6.2.1", "@sentry/aws-serverless": "8.45.0", "@sentry/core": "8.45.0", "@sentry/node": "8.45.0", diff --git a/dev-packages/node-integration-tests/scripts/use-ts-3_8.js b/dev-packages/node-integration-tests/scripts/use-ts-3_8.js new file mode 100644 index 000000000000..d759179f8e06 --- /dev/null +++ b/dev-packages/node-integration-tests/scripts/use-ts-3_8.js @@ -0,0 +1,39 @@ +/* eslint-disable no-console */ +const { execSync } = require('child_process'); +const { join } = require('path'); +const { readFileSync, writeFileSync } = require('fs'); + +const cwd = join(__dirname, '../../..'); + +// Newer versions of the Express types use syntax that isn't supported by TypeScript 3.8. +// We'll pin to the last version of those types that are compatible. +console.log('Pinning Express types to old versions...'); + +const packageJsonPath = join(cwd, 'package.json'); +const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + +if (!packageJson.resolutions) packageJson.resolutions = {}; +packageJson.resolutions['@types/express'] = '4.17.13'; +packageJson.resolutions['@types/express-serve-static-core'] = '4.17.30'; + +writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + +const tsVersion = '3.8'; + +console.log(`Installing typescript@${tsVersion}, and @types/node@14...`); + +execSync(`yarn add --dev --ignore-workspace-root-check typescript@${tsVersion} @types/node@^14`, { + stdio: 'inherit', + cwd, +}); + +console.log('Removing unsupported tsconfig options...'); + +const baseTscConfigPath = join(cwd, 'packages/typescript/tsconfig.json'); + +const tsConfig = require(baseTscConfigPath); + +// TS 3.8 fails build when it encounters a config option it does not understand, so we remove it :( +delete tsConfig.compilerOptions.noUncheckedIndexedAccess; + +writeFileSync(baseTscConfigPath, JSON.stringify(tsConfig, null, 2)); diff --git a/dev-packages/node-integration-tests/scripts/use-ts-version.js b/dev-packages/node-integration-tests/scripts/use-ts-version.js deleted file mode 100644 index 0b64d735436c..000000000000 --- a/dev-packages/node-integration-tests/scripts/use-ts-version.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable no-console */ -const { execSync } = require('child_process'); -const { join } = require('path'); -const { writeFileSync } = require('fs'); - -const cwd = join(__dirname, '../../..'); - -const tsVersion = process.argv[2] || '3.8'; - -console.log(`Installing typescript@${tsVersion}...`); - -execSync(`yarn add --dev --ignore-workspace-root-check typescript@${tsVersion}`, { stdio: 'inherit', cwd }); - -console.log('Removing unsupported tsconfig options...'); - -const baseTscConfigPath = join(cwd, 'packages/typescript/tsconfig.json'); - -const tsConfig = require(baseTscConfigPath); - -// TS 3.8 fails build when it encounteres a config option it does not understand, so we remove it :( -delete tsConfig.compilerOptions.noUncheckedIndexedAccess; - -writeFileSync(baseTscConfigPath, JSON.stringify(tsConfig, null, 2)); diff --git a/dev-packages/node-integration-tests/src/index.ts b/dev-packages/node-integration-tests/src/index.ts index 18c443203926..9c8971ea329d 100644 --- a/dev-packages/node-integration-tests/src/index.ts +++ b/dev-packages/node-integration-tests/src/index.ts @@ -29,6 +29,9 @@ export function startExpressServerAndSendPortToRunner(app: Express, port: number const server = app.listen(port || 0, () => { const address = server.address() as AddressInfo; + // @ts-expect-error If we write the port to the app we can read it within route handlers in tests + app.port = port || address.port; + // eslint-disable-next-line no-console console.log(`{"port":${port || address.port}}`); }); @@ -41,3 +44,11 @@ export function sendPortToRunner(port: number): void { // eslint-disable-next-line no-console console.log(`{"port":${port}}`); } + +/** + * Can be used to get the port of a running app, so requests can be sent to a server from within the server. + */ +export function getPortAppIsRunningOn(app: Express): number | undefined { + // @ts-expect-error It's not defined in the types but we'd like to read it. + return app.port; +} diff --git a/dev-packages/node-integration-tests/suites/anr/app-path.mjs b/dev-packages/node-integration-tests/suites/anr/app-path.mjs new file mode 100644 index 000000000000..97f28d07c59e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/anr/app-path.mjs @@ -0,0 +1,35 @@ +import * as assert from 'assert'; +import * as crypto from 'crypto'; +import * as path from 'path'; +import * as url from 'url'; + +import * as Sentry from '@sentry/node'; + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +setTimeout(() => { + process.exit(); +}, 10000); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, appRootPath: __dirname })], +}); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs new file mode 100644 index 000000000000..49c28cb21dbf --- /dev/null +++ b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs @@ -0,0 +1,35 @@ +import * as assert from 'assert'; +import * as crypto from 'crypto'; + +import * as Sentry from '@sentry/node'; + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +setTimeout(() => { + process.exit(); +}, 10000); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, maxAnrEvents: 2 })], +}); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); + +setTimeout(() => { + longWork(); +}, 4000); diff --git a/dev-packages/node-integration-tests/suites/anr/basic-session.js b/dev-packages/node-integration-tests/suites/anr/basic-session.js index c6415b6358da..9700131a6040 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic-session.js +++ b/dev-packages/node-integration-tests/suites/anr/basic-session.js @@ -11,7 +11,6 @@ Sentry.init({ dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], - autoSessionTracking: true, }); Sentry.setUser({ email: 'person@home.com' }); diff --git a/dev-packages/node-integration-tests/suites/anr/basic.js b/dev-packages/node-integration-tests/suites/anr/basic.js index e2adf0e8c60f..430058200b8f 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.js +++ b/dev-packages/node-integration-tests/suites/anr/basic.js @@ -12,7 +12,6 @@ setTimeout(() => { Sentry.init({ dsn: process.env.SENTRY_DSN, release: '1.0', - autoSessionTracking: false, integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], }); diff --git a/dev-packages/node-integration-tests/suites/anr/basic.mjs b/dev-packages/node-integration-tests/suites/anr/basic.mjs index 18777e5ecdbd..85b5cfb55c35 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.mjs +++ b/dev-packages/node-integration-tests/suites/anr/basic.mjs @@ -12,7 +12,6 @@ setTimeout(() => { Sentry.init({ dsn: process.env.SENTRY_DSN, release: '1.0', - autoSessionTracking: false, integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], }); @@ -30,3 +29,8 @@ function longWork() { setTimeout(() => { longWork(); }, 1000); + +// Ensure we only send one event even with multiple blocking events +setTimeout(() => { + longWork(); +}, 4000); diff --git a/dev-packages/node-integration-tests/suites/anr/forked.js b/dev-packages/node-integration-tests/suites/anr/forked.js index 06529096cca5..18720a7258af 100644 --- a/dev-packages/node-integration-tests/suites/anr/forked.js +++ b/dev-packages/node-integration-tests/suites/anr/forked.js @@ -10,7 +10,6 @@ setTimeout(() => { Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - autoSessionTracking: false, debug: true, integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], }); diff --git a/dev-packages/node-integration-tests/suites/anr/indefinite.mjs b/dev-packages/node-integration-tests/suites/anr/indefinite.mjs index d37f041b8c23..000c63a12cf3 100644 --- a/dev-packages/node-integration-tests/suites/anr/indefinite.mjs +++ b/dev-packages/node-integration-tests/suites/anr/indefinite.mjs @@ -10,7 +10,6 @@ setTimeout(() => { Sentry.init({ dsn: process.env.SENTRY_DSN, release: '1.0', - autoSessionTracking: false, integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], }); diff --git a/dev-packages/node-integration-tests/suites/anr/isolated.mjs b/dev-packages/node-integration-tests/suites/anr/isolated.mjs index 2f36575fbbd2..26ec9eaf4546 100644 --- a/dev-packages/node-integration-tests/suites/anr/isolated.mjs +++ b/dev-packages/node-integration-tests/suites/anr/isolated.mjs @@ -10,7 +10,6 @@ setTimeout(() => { Sentry.init({ dsn: process.env.SENTRY_DSN, release: '1.0', - autoSessionTracking: false, integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], }); diff --git a/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js b/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js index 01ee6f283819..2536c48553e7 100644 --- a/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js +++ b/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js @@ -4,7 +4,6 @@ function configureSentry() { Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - autoSessionTracking: false, debug: true, integrations: [Sentry.anrIntegration({ captureStackTrace: true })], }); diff --git a/dev-packages/node-integration-tests/suites/anr/should-exit.js b/dev-packages/node-integration-tests/suites/anr/should-exit.js index 5b3d23bf8cff..85ad4c508e17 100644 --- a/dev-packages/node-integration-tests/suites/anr/should-exit.js +++ b/dev-packages/node-integration-tests/suites/anr/should-exit.js @@ -4,7 +4,6 @@ function configureSentry() { Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - autoSessionTracking: false, debug: true, integrations: [Sentry.anrIntegration({ captureStackTrace: true })], }); diff --git a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js index 4f9fc9bc64db..b833dfde5eb6 100644 --- a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js +++ b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js @@ -13,7 +13,6 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, - autoSessionTracking: false, integrations: [anr], }); diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index b1750b308d28..ec980f07f123 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -1,5 +1,4 @@ import type { Event } from '@sentry/core'; -import { conditionalTest } from '../../utils'; import { cleanupChildProcesses, createRunner } from '../../utils/runner'; const ANR_EVENT = { @@ -31,20 +30,20 @@ const ANR_EVENT = { mechanism: { type: 'ANR' }, stacktrace: { frames: expect.arrayContaining([ - { + expect.objectContaining({ colno: expect.any(Number), lineno: expect.any(Number), filename: expect.any(String), function: '?', in_app: true, - }, - { + }), + expect.objectContaining({ colno: expect.any(Number), lineno: expect.any(Number), filename: expect.any(String), function: 'longWork', in_app: true, - }, + }), ]), }, }, @@ -101,13 +100,13 @@ const ANR_EVENT_WITH_DEBUG_META: Event = { { type: 'sourcemap', debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - code_file: expect.stringContaining('basic.'), + code_file: expect.stringContaining('basic'), }, ], }, }; -conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => { +describe('should report ANR when event loop blocked', () => { afterAll(() => { cleanupChildProcesses(); }); @@ -123,6 +122,34 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => .start(done); }); + test('Custom appRootPath', done => { + const ANR_EVENT_WITH_SPECIFIC_DEBUG_META: Event = { + ...ANR_EVENT_WITH_SCOPE, + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: 'app:///app-path.mjs', + }, + ], + }, + }; + + createRunner(__dirname, 'app-path.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_SPECIFIC_DEBUG_META }) + .start(done); + }); + + test('multiple events via maxAnrEvents', done => { + createRunner(__dirname, 'basic-multiple.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .start(done); + }); + test('blocked indefinitely', done => { createRunner(__dirname, 'indefinite.mjs').withMockSentryServer().expect({ event: ANR_EVENT }).start(done); }); diff --git a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs index 903470806ad9..298952d58ced 100644 --- a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs +++ b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs @@ -9,6 +9,7 @@ const __dirname = new URL('.', import.meta.url).pathname; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + integrations: [Sentry.childProcessIntegration({ captureWorkerErrors: false })], transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/child-process/child.js b/dev-packages/node-integration-tests/suites/child-process/child.js new file mode 100644 index 000000000000..cb1937007297 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/child.js @@ -0,0 +1,3 @@ +setTimeout(() => { + throw new Error('Test error'); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/child-process/child.mjs b/dev-packages/node-integration-tests/suites/child-process/child.mjs new file mode 100644 index 000000000000..cb1937007297 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/child.mjs @@ -0,0 +1,3 @@ +setTimeout(() => { + throw new Error('Test error'); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/child-process/fork.js b/dev-packages/node-integration-tests/suites/child-process/fork.js new file mode 100644 index 000000000000..c6e5cd3f0b7f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/fork.js @@ -0,0 +1,18 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const path = require('path'); +const { fork } = require('child_process'); + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +// eslint-disable-next-line no-unused-vars +const _child = fork(path.join(__dirname, 'child.mjs')); + +setTimeout(() => { + throw new Error('Exiting main process'); +}, 3000); diff --git a/dev-packages/node-integration-tests/suites/child-process/fork.mjs b/dev-packages/node-integration-tests/suites/child-process/fork.mjs new file mode 100644 index 000000000000..88503fa887a9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/fork.mjs @@ -0,0 +1,19 @@ +import { fork } from 'child_process'; +import * as path from 'path'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +const __dirname = new URL('.', import.meta.url).pathname; + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +const _child = fork(path.join(__dirname, 'child.mjs')); + +setTimeout(() => { + throw new Error('Exiting main process'); +}, 3000); diff --git a/dev-packages/node-integration-tests/suites/child-process/test.ts b/dev-packages/node-integration-tests/suites/child-process/test.ts new file mode 100644 index 000000000000..9b9064dacf3e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/test.ts @@ -0,0 +1,65 @@ +import type { Event } from '@sentry/core'; +import { conditionalTest } from '../../utils'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +const WORKER_EVENT: Event = { + exception: { + values: [ + { + type: 'Error', + value: 'Test error', + mechanism: { + type: 'instrument', + handled: false, + data: { + threadId: expect.any(String), + }, + }, + }, + ], + }, +}; + +const CHILD_EVENT: Event = { + exception: { + values: [ + { + type: 'Error', + value: 'Exiting main process', + }, + ], + }, + breadcrumbs: [ + { + category: 'child_process', + message: "Child process exited with code '1'", + level: 'warning', + }, + ], +}; + +describe('should capture child process events', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + conditionalTest({ min: 20 })('worker', () => { + test('ESM', done => { + createRunner(__dirname, 'worker.mjs').expect({ event: WORKER_EVENT }).start(done); + }); + + test('CJS', done => { + createRunner(__dirname, 'worker.js').expect({ event: WORKER_EVENT }).start(done); + }); + }); + + conditionalTest({ min: 20 })('fork', () => { + test('ESM', done => { + createRunner(__dirname, 'fork.mjs').expect({ event: CHILD_EVENT }).start(done); + }); + + test('CJS', done => { + createRunner(__dirname, 'fork.js').expect({ event: CHILD_EVENT }).start(done); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/child-process/worker.js b/dev-packages/node-integration-tests/suites/child-process/worker.js new file mode 100644 index 000000000000..99b645d9001c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/worker.js @@ -0,0 +1,18 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const path = require('path'); +const { Worker } = require('worker_threads'); + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +// eslint-disable-next-line no-unused-vars +const _worker = new Worker(path.join(__dirname, 'child.js')); + +setTimeout(() => { + process.exit(); +}, 3000); diff --git a/dev-packages/node-integration-tests/suites/child-process/worker.mjs b/dev-packages/node-integration-tests/suites/child-process/worker.mjs new file mode 100644 index 000000000000..dcca0bcc4105 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/worker.mjs @@ -0,0 +1,19 @@ +import * as path from 'path'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; +import { Worker } from 'worker_threads'; + +const __dirname = new URL('.', import.meta.url).pathname; + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +const _worker = new Worker(path.join(__dirname, 'child.mjs')); + +setTimeout(() => { + process.exit(); +}, 3000); diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts index 363b8f268cd2..06cac1581bfe 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts @@ -6,6 +6,7 @@ afterAll(() => { test('should record client report for beforeSend', done => { createRunner(__dirname, 'scenario.ts') + .unignore('client_report') .expect({ client_report: { discarded_events: [ diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts index 803f1c09bafe..7dab2e904780 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts @@ -6,6 +6,7 @@ afterAll(() => { test('should record client report for event processors', done => { createRunner(__dirname, 'scenario.ts') + .unignore('client_report') .expect({ client_report: { discarded_events: [ diff --git a/dev-packages/node-integration-tests/suites/client-reports/periodic-send/test.ts b/dev-packages/node-integration-tests/suites/client-reports/periodic-send/test.ts index 0364f3ea01f0..65463193e1f5 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/periodic-send/test.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/periodic-send/test.ts @@ -6,6 +6,7 @@ afterAll(() => { test('should flush client reports automatically after the timeout interval', done => { createRunner(__dirname, 'scenario.ts') + .unignore('client_report') .expect({ client_report: { discarded_events: [ diff --git a/dev-packages/node-integration-tests/suites/contextLines/instrument.mjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs similarity index 88% rename from dev-packages/node-integration-tests/suites/contextLines/instrument.mjs rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs index b3b8dda3720c..89dcca029527 100644 --- a/dev-packages/node-integration-tests/suites/contextLines/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs @@ -4,6 +4,5 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - autoSessionTracking: false, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/contextLines/scenario with space.cjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs similarity index 91% rename from dev-packages/node-integration-tests/suites/contextLines/scenario with space.cjs rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs index 9e9c52cd0928..41618eb3fee5 100644 --- a/dev-packages/node-integration-tests/suites/contextLines/scenario with space.cjs +++ b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs @@ -4,7 +4,6 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - autoSessionTracking: false, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/contextLines/scenario with space.mjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/contextLines/scenario with space.mjs rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs diff --git a/dev-packages/node-integration-tests/suites/contextLines/test.ts b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts similarity index 90% rename from dev-packages/node-integration-tests/suites/contextLines/test.ts rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts index 1912f0b57f04..3407d1a14f9c 100644 --- a/dev-packages/node-integration-tests/suites/contextLines/test.ts +++ b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts @@ -1,8 +1,7 @@ import { join } from 'path'; -import { conditionalTest } from '../../utils'; -import { createRunner } from '../../utils/runner'; +import { createRunner } from '../../../utils/runner'; -conditionalTest({ min: 18 })('ContextLines integration in ESM', () => { +describe('ContextLines integration in ESM', () => { test('reads encoded context lines from filenames with spaces', done => { expect.assertions(1); const instrumentPath = join(__dirname, 'instrument.mjs'); @@ -56,17 +55,17 @@ describe('ContextLines integration in CJS', () => { filename: expect.stringMatching(/\/scenario with space.cjs$/), context_line: "Sentry.captureException(new Error('Test Error'));", pre_context: [ + '', 'Sentry.init({', " dsn: 'https://public@dsn.ingest.sentry.io/1337',", " release: '1.0',", - ' autoSessionTracking: false,', ' transport: loggingTransport,', '});', '', ], post_context: ['', '// some more post context'], colno: 25, - lineno: 11, + lineno: 10, function: 'Object.?', in_app: true, module: 'scenario with space', diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/nested-file.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/nested-file.ts new file mode 100644 index 000000000000..47aec48484b7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/nested-file.ts @@ -0,0 +1,5 @@ +import * as Sentry from '@sentry/node'; + +export function captureException(i: number): void { + Sentry.captureException(new Error(`error in loop ${i}`)); +} diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts new file mode 100644 index 000000000000..c48fae3e2e2e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts @@ -0,0 +1,7 @@ +import { captureException } from './nested-file'; + +export function runSentry(): void { + for (let i = 0; i < 10; i++) { + captureException(i); + } +} diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts new file mode 100644 index 000000000000..0ca16a75fae2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts @@ -0,0 +1,30 @@ +import { execSync } from 'node:child_process'; +import * as path from 'node:path'; + +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +import { runSentry } from './other-file'; + +runSentry(); + +const lsofOutput = execSync(`lsof -p ${process.pid}`, { encoding: 'utf8' }); +const lsofTable = lsofOutput.split('\n'); +const mainPath = __dirname.replace(`${path.sep}suites${path.sep}contextLines${path.sep}memory-leak`, ''); +const numberOfLsofEntriesWithMainPath = lsofTable.filter(entry => entry.includes(mainPath)); + +// There should only be a single entry with the main path, otherwise we are leaking file handles from the +// context lines integration. +if (numberOfLsofEntriesWithMainPath.length > 1) { + // eslint-disable-next-line no-console + console.error('Leaked file handles detected'); + // eslint-disable-next-line no-console + console.error(lsofTable); + process.exit(1); +} diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/test.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/test.ts new file mode 100644 index 000000000000..0ec5ea95e896 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/test.ts @@ -0,0 +1,16 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('ContextLines integration in CJS', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + // Regression test for: https://github.com/getsentry/sentry-javascript/issues/14892 + test('does not leak open file handles', done => { + createRunner(__dirname, 'scenario.ts') + .expectN(10, { + event: {}, + }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts b/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts index 12416fd056ca..17cfcf810482 100644 --- a/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts @@ -5,7 +5,6 @@ import { CronJob } from 'cron'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - autoSessionTracking: false, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts index 8fe4f1bd34c5..57e5e7123fd7 100644 --- a/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts @@ -5,7 +5,6 @@ import * as cron from 'node-cron'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - autoSessionTracking: false, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts index badcc87fbbce..a85f50701341 100644 --- a/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts @@ -5,7 +5,6 @@ import * as schedule from 'node-schedule'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - autoSessionTracking: false, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs index 6b20155aea38..fbd43f8540dc 100644 --- a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs +++ b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs @@ -11,9 +11,7 @@ new iitm.Hook((_, name) => { Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - autoSessionTracking: false, transport: loggingTransport, - registerEsmLoaderHooks: { onlyIncludeInstrumentedModules: true }, }); await import('./sub-module.mjs'); diff --git a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts index d1584c2ea32d..937edc76cd5c 100644 --- a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts +++ b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts @@ -1,14 +1,13 @@ import { spawnSync } from 'child_process'; import { join } from 'path'; -import { conditionalTest } from '../../../utils'; import { cleanupChildProcesses } from '../../../utils/runner'; afterAll(() => { cleanupChildProcesses(); }); -conditionalTest({ min: 18 })('import-in-the-middle', () => { - test('onlyIncludeInstrumentedModules', () => { +describe('import-in-the-middle', () => { + test('should only instrument modules that we have instrumentation for', () => { const result = spawnSync('node', [join(__dirname, 'app.mjs')], { encoding: 'utf-8' }); expect(result.stderr).not.toMatch('should be the only hooked modules but we just hooked'); }); diff --git a/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs b/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs index 7f4316dce907..5b2300d7037c 100644 --- a/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs +++ b/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs @@ -4,7 +4,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - autoSessionTracking: false, integrations: [Sentry.modulesIntegration()], transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/esm/modules-integration/test.ts b/dev-packages/node-integration-tests/suites/esm/modules-integration/test.ts index 556ec1d52a57..eaee003781f3 100644 --- a/dev-packages/node-integration-tests/suites/esm/modules-integration/test.ts +++ b/dev-packages/node-integration-tests/suites/esm/modules-integration/test.ts @@ -1,11 +1,10 @@ -import { conditionalTest } from '../../../utils'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; afterAll(() => { cleanupChildProcesses(); }); -conditionalTest({ min: 18 })('modulesIntegration', () => { +describe('modulesIntegration', () => { test('does not crash ESM setups', done => { createRunner(__dirname, 'app.mjs').ensureNoErrorOutput().start(done); }); diff --git a/dev-packages/node-integration-tests/suites/express/requestUser/server.js b/dev-packages/node-integration-tests/suites/express/requestUser/server.js new file mode 100644 index 000000000000..d93d22905506 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/requestUser/server.js @@ -0,0 +1,49 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + debug: true, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.use((req, _res, next) => { + // We simulate this, which would in other cases be done by some middleware + req.user = { + id: '1', + email: 'test@sentry.io', + }; + + next(); +}); + +app.get('/test1', (_req, _res) => { + throw new Error('error_1'); +}); + +app.use((_req, _res, next) => { + Sentry.setUser({ + id: '2', + email: 'test2@sentry.io', + }); + + next(); +}); + +app.get('/test2', (_req, _res) => { + throw new Error('error_2'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/requestUser/test.ts b/dev-packages/node-integration-tests/suites/express/requestUser/test.ts new file mode 100644 index 000000000000..2a9fc58a7c18 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/requestUser/test.ts @@ -0,0 +1,42 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('express user handling', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('ignores user from request', done => { + expect.assertions(2); + + createRunner(__dirname, 'server.js') + .expect({ + event: event => { + expect(event.user).toBeUndefined(); + expect(event.exception?.values?.[0]?.value).toBe('error_1'); + }, + }) + .start(done) + .makeRequest('get', '/test1', { expectError: true }); + }); + + test('using setUser in middleware works', done => { + createRunner(__dirname, 'server.js') + .expect({ + event: { + user: { + id: '2', + email: 'test2@sentry.io', + }, + exception: { + values: [ + { + value: 'error_2', + }, + ], + }, + }, + }) + .start(done) + .makeRequest('get', '/test2', { expectError: true }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts index 0ee5ca2204f5..513cf6146d0f 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts @@ -1,3 +1,4 @@ +import { parseBaggageHeader } from '@sentry/core'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; import type { TestAPIResponse } from '../server'; @@ -29,7 +30,7 @@ test('Should propagate sentry trace baggage data from an incoming to an outgoing const response = await runner.makeRequest('get', '/test/express', { headers: { 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great,sentry-sample_rand=0.42', }, }); @@ -37,7 +38,7 @@ test('Should propagate sentry trace baggage data from an incoming to an outgoing expect(response).toMatchObject({ test_data: { host: 'somewhere.not.sentry', - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,sentry-sample_rand=0.42', }, }); }); @@ -102,14 +103,20 @@ test('Should populate and propagate sentry baggage if sentry-trace header does n const response = await runner.makeRequest('get', '/test/express'); expect(response).toBeDefined(); - expect(response).toMatchObject({ - test_data: { - host: 'somewhere.not.sentry', - // TraceId changes, hence we only expect that the string contains the traceid key - baggage: expect.stringMatching( - /sentry-environment=prod,sentry-release=1.0,sentry-public_key=public,sentry-trace_id=[\S]*,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress/, - ), - }, + + const parsedBaggage = parseBaggageHeader(response?.test_data.baggage); + + expect(response?.test_data.host).toBe('somewhere.not.sentry'); + expect(parsedBaggage).toStrictEqual({ + 'sentry-environment': 'prod', + 'sentry-release': '1.0', + 'sentry-public_key': 'public', + // TraceId changes, hence we only expect that the string contains the traceid key + 'sentry-trace_id': expect.stringMatching(/[\S]*/), + 'sentry-sample_rand': expect.stringMatching(/[\S]*/), + 'sentry-sample_rate': '1', + 'sentry-sampled': 'true', + 'sentry-transaction': 'GET /test/express', }); }); @@ -123,13 +130,18 @@ test('Should populate Sentry and ignore 3rd party content if sentry-trace header }); expect(response).toBeDefined(); - expect(response).toMatchObject({ - test_data: { - host: 'somewhere.not.sentry', - // TraceId changes, hence we only expect that the string contains the traceid key - baggage: expect.stringMatching( - /sentry-environment=prod,sentry-release=1.0,sentry-public_key=public,sentry-trace_id=[\S]*,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress/, - ), - }, + expect(response?.test_data.host).toBe('somewhere.not.sentry'); + + const parsedBaggage = parseBaggageHeader(response?.test_data.baggage); + expect(parsedBaggage).toStrictEqual({ + 'sentry-environment': 'prod', + 'sentry-release': '1.0', + 'sentry-public_key': 'public', + // TraceId changes, hence we only expect that the string contains the traceid key + 'sentry-trace_id': expect.stringMatching(/[\S]*/), + 'sentry-sample_rand': expect.stringMatching(/[\S]*/), + 'sentry-sample_rate': '1', + 'sentry-sampled': 'true', + 'sentry-transaction': 'GET /test/express', }); }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts index 5a052a454b56..72b6a7139f35 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts @@ -12,9 +12,9 @@ test('should attach a baggage header to an outgoing request.', async () => { expect(response).toBeDefined(); - const baggage = response?.test_data.baggage?.split(',').sort(); + const baggage = response?.test_data.baggage?.split(','); - expect(baggage).toEqual([ + [ 'sentry-environment=prod', 'sentry-public_key=public', 'sentry-release=1.0', @@ -22,7 +22,10 @@ test('should attach a baggage header to an outgoing request.', async () => { 'sentry-sampled=true', 'sentry-trace_id=__SENTRY_TRACE_ID__', 'sentry-transaction=GET%20%2Ftest%2Fexpress', - ]); + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), + ].forEach(item => { + expect(baggage).toContainEqual(item); + }); expect(response).toMatchObject({ test_data: { diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts index 0e083f5c2dc6..ebf2a15bedf4 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts @@ -31,6 +31,7 @@ test('should ignore sentry-values in `baggage` header of a third party vendor an 'other=vendor', 'sentry-environment=myEnv', 'sentry-release=2.1.0', + expect.stringMatching(/sentry-sample_rand=[0-9]+/), 'sentry-sample_rate=0.54', 'third=party', ]); @@ -58,6 +59,7 @@ test('should ignore sentry-values in `baggage` header of a third party vendor an 'sentry-environment=prod', 'sentry-public_key=public', 'sentry-release=1.0', + expect.stringMatching(/sentry-sample_rand=[0-9]+/), 'sentry-sample_rate=1', 'sentry-sampled=true', expect.stringMatching(/sentry-trace_id=[0-9a-f]{32}/), diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts index 2403da850d9d..0beecb54a905 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts @@ -11,7 +11,7 @@ test('should merge `baggage` header of a third party vendor with the Sentry DSC const response = await runner.makeRequest('get', '/test/express', { headers: { 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,sentry-sample_rand=0.42', }, }); @@ -19,7 +19,7 @@ test('should merge `baggage` header of a third party vendor with the Sentry DSC expect(response).toMatchObject({ test_data: { host: 'somewhere.not.sentry', - baggage: 'other=vendor,foo=bar,third=party,sentry-release=2.0.0,sentry-environment=myEnv', + baggage: 'other=vendor,foo=bar,third=party,sentry-release=2.0.0,sentry-environment=myEnv,sentry-sample_rand=0.42', }, }); }); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalizedRequest.js b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalizedRequest.js new file mode 100644 index 000000000000..da31780f2c5f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalizedRequest.js @@ -0,0 +1,34 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + tracesSampler: samplingContext => { + // The sampling decision is based on whether the data in `normalizedRequest` is available --> this is what we want to test for + return ( + samplingContext.normalizedRequest.url.includes('/test-normalized-request?query=123') && + samplingContext.normalizedRequest.method && + samplingContext.normalizedRequest.query_string === 'query=123' && + !!samplingContext.normalizedRequest.headers + ); + }, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/test-normalized-request', (_req, res) => { + res.send('Success'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js index c096871cb755..b60ea07b636f 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js +++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js @@ -15,7 +15,6 @@ Sentry.init({ samplingContext.attributes['http.method'] === 'GET' ); }, - debug: true, }); // express must be required after Sentry is initialized diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts index a19299787f91..07cc8d094d8f 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts +++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts @@ -22,3 +22,23 @@ describe('express tracesSampler', () => { }); }); }); + +describe('express tracesSampler includes normalizedRequest data', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('CJS', () => { + test('correctly samples & passes data to tracesSampler', done => { + const runner = createRunner(__dirname, 'scenario-normalizedRequest.js') + .expect({ + transaction: { + transaction: 'GET /test-normalized-request', + }, + }) + .start(done); + + runner.makeRequest('get', '/test-normalized-request?query=123'); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/updateName/server.js b/dev-packages/node-integration-tests/suites/express/tracing/updateName/server.js new file mode 100644 index 000000000000..c98e17276d92 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/tracing/updateName/server.js @@ -0,0 +1,58 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + // disable attaching headers to /test/* endpoints + tracePropagationTargets: [/^(?!.*test).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); + +app.get('/test/:id/span-updateName', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + rootSpan.updateName('new-name'); + res.send({ response: 'response 1' }); +}); + +app.get('/test/:id/span-updateName-source', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + rootSpan.updateName('new-name'); + rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + res.send({ response: 'response 2' }); +}); + +app.get('/test/:id/updateSpanName', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + Sentry.updateSpanName(rootSpan, 'new-name'); + res.send({ response: 'response 3' }); +}); + +app.get('/test/:id/updateSpanNameAndSource', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + Sentry.updateSpanName(rootSpan, 'new-name'); + rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'component'); + res.send({ response: 'response 4' }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts new file mode 100644 index 000000000000..c6345713fd7e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts @@ -0,0 +1,94 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('express tracing', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('CJS', () => { + // This test documents the unfortunate behaviour of using `span.updateName` on the server-side. + // For http.server root spans (which is the root span on the server 99% of the time), Otel's http instrumentation + // calls `span.updateName` and overwrites whatever the name was set to before (by us or by users). + test("calling just `span.updateName` doesn't update the final name in express (missing source)", done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'GET /test/:id/span-updateName', + transaction_info: { + source: 'route', + }, + }, + }) + .start(done) + .makeRequest('get', '/test/123/span-updateName'); + }); + + // Also calling `updateName` AND setting a source doesn't change anything - Otel has no concept of source, this is sentry-internal. + // Therefore, only the source is updated but the name is still overwritten by Otel. + test("calling `span.updateName` and setting attribute source doesn't update the final name in express but it updates the source", done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'GET /test/:id/span-updateName-source', + transaction_info: { + source: 'custom', + }, + }, + }) + .start(done) + .makeRequest('get', '/test/123/span-updateName-source'); + }); + + // This test documents the correct way to update the span name (and implicitly the source) in Node: + test('calling `Sentry.updateSpanName` updates the final name and source in express', done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: txnEvent => { + expect(txnEvent).toMatchObject({ + transaction: 'new-name', + transaction_info: { + source: 'custom', + }, + contexts: { + trace: { + op: 'http.server', + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }); + // ensure we delete the internal attribute once we're done with it + expect(txnEvent.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + }, + }) + .start(done) + .makeRequest('get', '/test/123/updateSpanName'); + }); + }); + + // This test documents the correct way to update the span name (and implicitly the source) in Node: + test('calling `Sentry.updateSpanName` and setting source subsequently updates the final name and sets correct source', done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: txnEvent => { + expect(txnEvent).toMatchObject({ + transaction: 'new-name', + transaction_info: { + source: 'component', + }, + contexts: { + trace: { + op: 'http.server', + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, + }, + }, + }); + // ensure we delete the internal attribute once we're done with it + expect(txnEvent.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + }, + }) + .start(done) + .makeRequest('get', '/test/123/updateSpanNameAndSource'); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/metrics/should-exit-forced.js b/dev-packages/node-integration-tests/suites/metrics/should-exit-forced.js deleted file mode 100644 index 2621828973ab..000000000000 --- a/dev-packages/node-integration-tests/suites/metrics/should-exit-forced.js +++ /dev/null @@ -1,19 +0,0 @@ -const Sentry = require('@sentry/node'); - -function configureSentry() { - Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - autoSessionTracking: false, - }); - - Sentry.metrics.increment('test'); -} - -async function main() { - configureSentry(); - await new Promise(resolve => setTimeout(resolve, 1000)); - process.exit(0); -} - -main(); diff --git a/dev-packages/node-integration-tests/suites/metrics/should-exit.js b/dev-packages/node-integration-tests/suites/metrics/should-exit.js deleted file mode 100644 index 01a6f0194507..000000000000 --- a/dev-packages/node-integration-tests/suites/metrics/should-exit.js +++ /dev/null @@ -1,18 +0,0 @@ -const Sentry = require('@sentry/node'); - -function configureSentry() { - Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - autoSessionTracking: false, - }); - - Sentry.metrics.increment('test'); -} - -async function main() { - configureSentry(); - await new Promise(resolve => setTimeout(resolve, 1000)); -} - -main(); diff --git a/dev-packages/node-integration-tests/suites/metrics/test.ts b/dev-packages/node-integration-tests/suites/metrics/test.ts deleted file mode 100644 index 2c3cc350eeba..000000000000 --- a/dev-packages/node-integration-tests/suites/metrics/test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createRunner } from '../../utils/runner'; - -describe('metrics', () => { - test('should exit', done => { - const runner = createRunner(__dirname, 'should-exit.js').start(); - - setTimeout(() => { - expect(runner.childHasExited()).toBe(true); - done(); - }, 5_000); - }); - - test('should exit forced', done => { - const runner = createRunner(__dirname, 'should-exit-forced.js').start(); - - setTimeout(() => { - expect(runner.childHasExited()).toBe(true); - done(); - }, 5_000); - }); -}); diff --git a/dev-packages/node-integration-tests/suites/no-code/test.ts b/dev-packages/node-integration-tests/suites/no-code/test.ts index dfaae9de7cdc..fdcd5bd25fc6 100644 --- a/dev-packages/node-integration-tests/suites/no-code/test.ts +++ b/dev-packages/node-integration-tests/suites/no-code/test.ts @@ -1,4 +1,3 @@ -import { conditionalTest } from '../../utils'; import { cleanupChildProcesses, createRunner } from '../../utils/runner'; const EVENT = { @@ -25,7 +24,7 @@ describe('no-code init', () => { .start(done); }); - conditionalTest({ min: 18 })('--import', () => { + describe('--import', () => { test('ESM', done => { createRunner(__dirname, 'app.mjs') .withFlags('--import=@sentry/node/init') diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index 779b341d9f40..7ed9d352474a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -37,7 +37,7 @@ const EXPECTED_LOCAL_VARIABLES_EVENT = { }, }; -conditionalTest({ min: 18 })('LocalVariables integration', () => { +describe('LocalVariables integration', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts index 86b3bf6d9d22..0370b123cab2 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts @@ -1,11 +1,42 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { cleanupChildProcesses(); }); -test('should send a manually started root span', done => { +test('sends a manually started root span with source custom', done => { createRunner(__dirname, 'scenario.ts') - .expect({ transaction: { transaction: 'test_span' } }) + .expect({ + transaction: { + transaction: 'test_span', + transaction_info: { source: 'custom' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }, + }) + .start(done); +}); + +test("doesn't change the name for manually started spans even if attributes triggering inference are set", done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'test_span', + transaction_info: { source: 'custom' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }, + }) .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts index e352fff5c02c..fada0ea3aad4 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts @@ -10,8 +10,8 @@ Sentry.init({ Sentry.getCurrentScope().setPropagationContext({ parentSpanId: '1234567890123456', - spanId: '123456789012345x', traceId: '12345678901234567890123456789012', + sampleRand: Math.random(), }); const spanIdTraceId = Sentry.startSpan( diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts index 7c4f702f5df8..ca0431f2318f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts @@ -11,8 +11,8 @@ Sentry.init({ Sentry.withScope(scope => { scope.setPropagationContext({ parentSpanId: '1234567890123456', - spanId: '123456789012345x', traceId: '12345678901234567890123456789012', + sampleRand: Math.random(), }); const spanIdTraceId = Sentry.startSpan( diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts new file mode 100644 index 000000000000..ea30608c1c5c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts @@ -0,0 +1,16 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.startSpan( + { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + (span: Sentry.Span) => { + span.updateName('new name'); + }, +); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/test.ts new file mode 100644 index 000000000000..676071926b91 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/test.ts @@ -0,0 +1,24 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('updates the span name when calling `span.updateName`', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'new name', + transaction_info: { source: 'url' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, + }, + }, + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts new file mode 100644 index 000000000000..ecf7670fa23d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts @@ -0,0 +1,16 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.startSpan( + { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + (span: Sentry.Span) => { + Sentry.updateSpanName(span, 'new name'); + }, +); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts new file mode 100644 index 000000000000..c5b325fc0ea2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts @@ -0,0 +1,24 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('updates the span name and source when calling `updateSpanName`', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'new name', + transaction_info: { source: 'custom' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts index ffa693c4752d..63706d2bc9bc 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts @@ -30,10 +30,12 @@ test('should report finished spans as children of the root transaction.', done = { description: 'span_3', parent_span_id: rootSpanId, + data: {}, }, { description: 'span_5', parent_span_id: span3Id, + data: {}, }, ] as SpanJSON[], }); diff --git a/dev-packages/node-integration-tests/suites/sessions/server.ts b/dev-packages/node-integration-tests/suites/sessions/server.ts index 62b154accd45..df2587aacfd4 100644 --- a/dev-packages/node-integration-tests/suites/sessions/server.ts +++ b/dev-packages/node-integration-tests/suites/sessions/server.ts @@ -1,11 +1,16 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; -import type { SessionFlusher } from '@sentry/core'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + integrations: [ + Sentry.httpIntegration({ + // Flush after 2 seconds (to avoid waiting for the default 60s) + sessionFlushingDelayMS: 2_000, + }), + ], }); import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; @@ -13,14 +18,6 @@ import express from 'express'; const app = express(); -// eslint-disable-next-line deprecation/deprecation -const flusher = (Sentry.getClient() as Sentry.NodeClient)['_sessionFlusher'] as SessionFlusher; - -// Flush after 2 seconds (to avoid waiting for the default 60s) -setTimeout(() => { - flusher?.flush(); -}, 2000); - app.get('/test/success', (_req, res) => { res.send('Success!'); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ai/test.ts b/dev-packages/node-integration-tests/suites/tracing/ai/test.ts index e269f9da9db3..bc263e9fc610 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ai/test.ts @@ -1,8 +1,7 @@ -import { conditionalTest } from '../../../utils'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; // `ai` SDK only support Node 18+ -conditionalTest({ min: 18 })('ai', () => { +describe('ai', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/test.ts b/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/test.ts index 82f86baa835f..f669f50f5d7b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/test.ts @@ -17,6 +17,7 @@ test('adds current transaction name to baggage when the txn name is high-quality 'sentry-environment=production', 'sentry-public_key=public', 'sentry-release=1.0', + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), 'sentry-sample_rate=1', 'sentry-sampled=true', `sentry-trace_id=${traceId}`, @@ -27,6 +28,7 @@ test('adds current transaction name to baggage when the txn name is high-quality 'sentry-environment=production', 'sentry-public_key=public', 'sentry-release=1.0', + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), 'sentry-sample_rate=1', 'sentry-sampled=true', `sentry-trace_id=${traceId}`, @@ -38,6 +40,7 @@ test('adds current transaction name to baggage when the txn name is high-quality 'sentry-environment=production', 'sentry-public_key=public', 'sentry-release=1.0', + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), 'sentry-sample_rate=1', 'sentry-sampled=true', `sentry-trace_id=${traceId}`, @@ -68,6 +71,7 @@ test('adds current transaction name to trace envelope header when the txn name i sample_rate: '1', sampled: 'true', trace_id: expect.stringMatching(/[a-f0-9]{32}/), + sample_rand: expect.any(String), }, }, }) @@ -81,6 +85,7 @@ test('adds current transaction name to trace envelope header when the txn name i sampled: 'true', trace_id: expect.stringMatching(/[a-f0-9]{32}/), transaction: 'updated-name-1', + sample_rand: expect.any(String), }, }, }) @@ -94,6 +99,7 @@ test('adds current transaction name to trace envelope header when the txn name i sampled: 'true', trace_id: expect.stringMatching(/[a-f0-9]{32}/), transaction: 'updated-name-2', + sample_rand: expect.any(String), }, }, }) @@ -107,6 +113,7 @@ test('adds current transaction name to trace envelope header when the txn name i sampled: 'true', trace_id: expect.stringMatching(/[a-f0-9]{32}/), transaction: 'updated-name-2', + sample_rand: expect.any(String), }, }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts index 8ac2dd53a089..105722a43239 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts @@ -10,7 +10,9 @@ test('envelope header for error event during active unsampled span is correct', public_key: 'public', environment: 'production', release: '1.0', + sample_rate: '0', sampled: 'false', + sample_rand: expect.any(String), }, }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts index 6749f275035b..51d62deb75af 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts @@ -13,6 +13,7 @@ test('envelope header for error event during active span is correct', done => { sample_rate: '1', sampled: 'true', transaction: 'test span', + sample_rand: expect.any(String), }, }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js b/dev-packages/node-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js new file mode 100644 index 000000000000..87ed1bc64f76 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js @@ -0,0 +1,32 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + // disable attaching headers to /test/* endpoints + tracePropagationTargets: [/^(?!.*test).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); + +app.get('/test', (req, res) => { + res.send({ headers: req.headers }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/test.ts new file mode 100644 index 000000000000..55223beff4f6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/test.ts @@ -0,0 +1,31 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('tracesSampleRate propagation', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + const traceId = '12345678123456781234567812345678'; + + test('uses sample rate from incoming baggage header in trace envelope item', done => { + createRunner(__dirname, 'server.js') + .expectHeader({ + transaction: { + trace: { + sample_rate: '0.05', + sampled: 'true', + trace_id: traceId, + transaction: 'myTransaction', + sample_rand: '0.42', + }, + }, + }) + .start(done) + .makeRequest('get', '/test', { + headers: { + 'sentry-trace': `${traceId}-1234567812345678-1`, + baggage: `sentry-sample_rate=0.05,sentry-trace_id=${traceId},sentry-sampled=true,sentry-transaction=myTransaction,sentry-sample_rand=0.42`, + }, + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts index 592d75f30ae6..15088157994d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts @@ -12,6 +12,7 @@ test('envelope header for transaction event of route correct', done => { release: '1.0', sample_rate: '1', sampled: 'true', + sample_rand: expect.any(String), }, }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts index a7de2f95c965..8b5eb84392c9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts @@ -11,6 +11,7 @@ test('envelope header for transaction event with source=url correct', done => { release: '1.0', sample_rate: '1', sampled: 'true', + sample_rand: expect.any(String), }, }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction/test.ts index 3d4ff2d8d96a..1f26a45ffcac 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction/test.ts @@ -12,6 +12,7 @@ test('envelope header for transaction event is correct', done => { sample_rate: '1', sampled: 'true', transaction: 'test span', + sample_rand: expect.any(String), }, }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js index b42fa97fab08..8c5c1472dcfa 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js @@ -1,6 +1,5 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/node'); -const http = require('http'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -11,7 +10,7 @@ Sentry.init({ integrations: [ Sentry.httpIntegration({ ignoreOutgoingRequests: (url, request) => { - if (url === 'http://example.com/blockUrl') { + if (url === 'https://example.com/blockUrl') { return true; } @@ -24,6 +23,8 @@ Sentry.init({ ], }); +const https = require('https'); + // express must be required after Sentry is initialized const express = require('express'); const cors = require('cors'); @@ -34,16 +35,16 @@ const app = express(); app.use(cors()); app.get('/testUrl', (_req, response) => { - makeHttpRequest('http://example.com/blockUrl').then(() => { - makeHttpRequest('http://example.com/pass').then(() => { + makeHttpRequest('https://example.com/blockUrl').then(() => { + makeHttpRequest('https://example.com/pass').then(() => { response.send({ response: 'done' }); }); }); }); app.get('/testRequest', (_req, response) => { - makeHttpRequest('http://example.com/blockRequest').then(() => { - makeHttpRequest('http://example.com/pass').then(() => { + makeHttpRequest('https://example.com/blockRequest').then(() => { + makeHttpRequest('https://example.com/pass').then(() => { response.send({ response: 'done' }); }); }); @@ -55,7 +56,7 @@ startExpressServerAndSendPortToRunner(app); function makeHttpRequest(url) { return new Promise((resolve, reject) => { - http + https .get(url, res => { res.on('data', () => {}); res.on('end', () => { diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts index 7fc6a5f05efa..c3d72d4708c9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts @@ -137,11 +137,11 @@ describe('httpIntegration', () => { const requestSpans = event.spans?.filter(span => span.op === 'http.client'); expect(requestSpans).toHaveLength(1); - expect(requestSpans![0]?.description).toBe('GET http://example.com/pass'); + expect(requestSpans![0]?.description).toBe('GET https://example.com/pass'); const breadcrumbs = event.breadcrumbs?.filter(b => b.category === 'http'); expect(breadcrumbs).toHaveLength(1); - expect(breadcrumbs![0]?.data?.url).toEqual('http://example.com/pass'); + expect(breadcrumbs![0]?.data?.url).toEqual('https://example.com/pass'); }, }) .start(done); @@ -157,11 +157,11 @@ describe('httpIntegration', () => { const requestSpans = event.spans?.filter(span => span.op === 'http.client'); expect(requestSpans).toHaveLength(1); - expect(requestSpans![0]?.description).toBe('GET http://example.com/pass'); + expect(requestSpans![0]?.description).toBe('GET https://example.com/pass'); const breadcrumbs = event.breadcrumbs?.filter(b => b.category === 'http'); expect(breadcrumbs).toHaveLength(1); - expect(breadcrumbs![0]?.data?.url).toEqual('http://example.com/pass'); + expect(breadcrumbs![0]?.data?.url).toEqual('https://example.com/pass'); }, }) .start(done); diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts b/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts index 9abb7b1a631c..d4447255bf51 100644 --- a/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts @@ -5,6 +5,7 @@ describe('errors in TwP mode have same trace in trace context and getTraceData() cleanupChildProcesses(); }); + // In a request handler, the spanId is consistent inside of the request test('in incoming request', done => { createRunner(__dirname, 'server.js') .expect({ @@ -30,6 +31,7 @@ describe('errors in TwP mode have same trace in trace context and getTraceData() .makeRequest('get', '/test'); }); + // Outside of a request handler, the spanId is random test('outside of a request handler', done => { createRunner(__dirname, 'no-server.js') .expect({ @@ -41,11 +43,18 @@ describe('errors in TwP mode have same trace in trace context and getTraceData() const traceData = contexts?.traceData || {}; - expect(traceData['sentry-trace']).toEqual(`${trace_id}-${span_id}`); + expect(traceData['sentry-trace']).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}$/); + expect(traceData['sentry-trace']).toContain(`${trace_id}-`); + // span_id is a random span ID + expect(traceData['sentry-trace']).not.toContain(span_id); + expect(traceData.baggage).toContain(`sentry-trace_id=${trace_id}`); expect(traceData.baggage).not.toContain('sentry-sampled='); - expect(traceData.metaTags).toContain(``); + expect(traceData.metaTags).toMatch(//); + expect(traceData.metaTags).toContain(`/); - expect(html).toContain(''); + expect(html).toContain(''); }); test('injects tags with new trace if no incoming headers', async () => { diff --git a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js deleted file mode 100644 index 422fa4c504a5..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js +++ /dev/null @@ -1,50 +0,0 @@ -const { loggingTransport } = require('@sentry-internal/node-integration-tests'); -const Sentry = require('@sentry/node'); - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - transport: loggingTransport, -}); - -Sentry.startSpan( - { - name: 'Test Transaction', - op: 'transaction', - }, - () => { - Sentry.metrics.increment('root-counter', 1, { - tags: { - email: 'jon.doe@example.com', - }, - }); - Sentry.metrics.increment('root-counter', 1, { - tags: { - email: 'jane.doe@example.com', - }, - }); - - Sentry.startSpan( - { - name: 'Some other span', - op: 'transaction', - }, - () => { - Sentry.metrics.increment('root-counter'); - Sentry.metrics.increment('root-counter'); - Sentry.metrics.increment('root-counter', 2); - - Sentry.metrics.set('root-set', 'some-value'); - Sentry.metrics.set('root-set', 'another-value'); - Sentry.metrics.set('root-set', 'another-value'); - - Sentry.metrics.gauge('root-gauge', 42); - Sentry.metrics.gauge('root-gauge', 20); - - Sentry.metrics.distribution('root-distribution', 42); - Sentry.metrics.distribution('root-distribution', 20); - }, - ); - }, -); diff --git a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts deleted file mode 100644 index 94f5fdc30c70..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { createRunner } from '../../../utils/runner'; - -const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', - _metrics_summary: { - 'c:root-counter@none': [ - { - min: 1, - max: 1, - count: 1, - sum: 1, - tags: { - release: '1.0', - transaction: 'Test Transaction', - email: 'jon.doe@example.com', - }, - }, - { - min: 1, - max: 1, - count: 1, - sum: 1, - tags: { - release: '1.0', - transaction: 'Test Transaction', - email: 'jane.doe@example.com', - }, - }, - ], - }, - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'Some other span', - op: 'transaction', - _metrics_summary: { - 'c:root-counter@none': [ - { - min: 1, - max: 2, - count: 3, - sum: 4, - tags: { - release: '1.0', - transaction: 'Test Transaction', - }, - }, - ], - 's:root-set@none': [ - { - min: 0, - max: 1, - count: 3, - sum: 2, - tags: { - release: '1.0', - transaction: 'Test Transaction', - }, - }, - ], - 'g:root-gauge@none': [ - { - min: 20, - max: 42, - count: 2, - sum: 62, - tags: { - release: '1.0', - transaction: 'Test Transaction', - }, - }, - ], - 'd:root-distribution@none': [ - { - min: 20, - max: 42, - count: 2, - sum: 62, - tags: { - release: '1.0', - transaction: 'Test Transaction', - }, - }, - ], - }, - }), - ]), -}; - -test('Should add metric summaries to spans', done => { - createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts deleted file mode 100644 index 3dcf30f97b20..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ -import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 0, - transport: loggingTransport, - integrations: integrations => integrations.filter(i => i.name !== 'Express'), - debug: true, -}); - -import { Controller, Get, Injectable, Module, Param } from '@nestjs/common'; -import { BaseExceptionFilter, HttpAdapterHost, NestFactory } from '@nestjs/core'; - -const port = 3480; - -// Stop the process from exiting before the transaction is sent -// eslint-disable-next-line @typescript-eslint/no-empty-function -setInterval(() => {}, 1000); - -@Injectable() -class AppService { - getHello(): string { - return 'Hello World!'; - } -} - -@Controller() -class AppController { - constructor(private readonly appService: AppService) {} - - @Get('test-exception/:id') - async testException(@Param('id') id: string): Promise { - Sentry.captureException(new Error(`error with id ${id}`)); - } -} - -@Module({ - imports: [], - controllers: [AppController], - providers: [AppService], -}) -class AppModule {} - -async function run(): Promise { - const app = await NestFactory.create(AppModule); - const { httpAdapter } = app.get(HttpAdapterHost); - // eslint-disable-next-line deprecation/deprecation - Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter)); - await app.listen(port); - sendPortToRunner(port); -} - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/test.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/test.ts deleted file mode 100644 index 84b1aeef40c4..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { conditionalTest } from '../../../utils'; -import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; - -const { TS_VERSION } = process.env; -const isOldTS = TS_VERSION && TS_VERSION.startsWith('3.'); - -// This is required to run the test with ts-node and decorators -process.env.TS_NODE_PROJECT = `${__dirname}/tsconfig.json`; - -conditionalTest({ min: 16 })('nestjs auto instrumentation', () => { - afterAll(async () => { - cleanupChildProcesses(); - }); - - test("should assign scope's transactionName if spans are not sampled and express integration is disabled", done => { - if (isOldTS) { - // Skipping test on old TypeScript - return done(); - } - - createRunner(__dirname, 'scenario.ts') - .expect({ - event: { - exception: { - values: [ - { - value: 'error with id 456', - }, - ], - }, - transaction: 'GET /test-exception/:id', - }, - }) - .start(done) - .makeRequest('get', '/test-exception/456'); - }); -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/tsconfig.json b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/tsconfig.json deleted file mode 100644 index 84b8f8d6c44e..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "include": ["scenario.ts"], - "compilerOptions": { - "module": "commonjs", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "target": "ES2021", - } -} diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts deleted file mode 100644 index 6f4c9fa6955e..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ -import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 0, - transport: loggingTransport, -}); - -import { Controller, Get, Injectable, Module, Param } from '@nestjs/common'; -import { BaseExceptionFilter, HttpAdapterHost, NestFactory } from '@nestjs/core'; - -const port = 3460; - -// Stop the process from exiting before the transaction is sent -// eslint-disable-next-line @typescript-eslint/no-empty-function -setInterval(() => {}, 1000); - -@Injectable() -class AppService { - getHello(): string { - return 'Hello World!'; - } -} - -@Controller() -class AppController { - constructor(private readonly appService: AppService) {} - - @Get('test-exception/:id') - async testException(@Param('id') id: string): Promise { - Sentry.captureException(new Error(`error with id ${id}`)); - } -} - -@Module({ - imports: [], - controllers: [AppController], - providers: [AppService], -}) -class AppModule {} - -async function run(): Promise { - const app = await NestFactory.create(AppModule); - const { httpAdapter } = app.get(HttpAdapterHost); - // eslint-disable-next-line deprecation/deprecation - Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter)); - await app.listen(port); - sendPortToRunner(port); -} - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/test.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/test.ts deleted file mode 100644 index 1b366307eac6..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { conditionalTest } from '../../../utils'; -import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; - -const { TS_VERSION } = process.env; -const isOldTS = TS_VERSION && TS_VERSION.startsWith('3.'); - -// This is required to run the test with ts-node and decorators -process.env.TS_NODE_PROJECT = `${__dirname}/tsconfig.json`; - -conditionalTest({ min: 16 })('nestjs auto instrumentation', () => { - afterAll(async () => { - cleanupChildProcesses(); - }); - - test("should assign scope's transactionName if spans are not sampled", done => { - if (isOldTS) { - // Skipping test on old TypeScript - return done(); - } - - createRunner(__dirname, 'scenario.ts') - .expect({ - event: { - exception: { - values: [ - { - value: 'error with id 123', - }, - ], - }, - transaction: 'GET /test-exception/:id', - }, - }) - .start(done) - .makeRequest('get', '/test-exception/123'); - }); -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/tsconfig.json b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/tsconfig.json deleted file mode 100644 index 84b8f8d6c44e..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "include": ["scenario.ts"], - "compilerOptions": { - "module": "commonjs", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "target": "ES2021", - } -} diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts deleted file mode 100644 index 62e042a4bf7a..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ -import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1, - transport: loggingTransport, - integrations: integrations => integrations.filter(i => i.name !== 'Express'), - debug: true, -}); - -import { Controller, Get, Injectable, Module, Param } from '@nestjs/common'; -import { BaseExceptionFilter, HttpAdapterHost, NestFactory } from '@nestjs/core'; - -const port = 3470; - -// Stop the process from exiting before the transaction is sent -// eslint-disable-next-line @typescript-eslint/no-empty-function -setInterval(() => {}, 1000); - -@Injectable() -class AppService { - getHello(): string { - return 'Hello World!'; - } -} - -@Controller() -class AppController { - constructor(private readonly appService: AppService) {} - - @Get('test-exception/:id') - async testException(@Param('id') id: string): Promise { - Sentry.captureException(new Error(`error with id ${id}`)); - } -} - -@Module({ - imports: [], - controllers: [AppController], - providers: [AppService], -}) -class AppModule {} - -async function run(): Promise { - const app = await NestFactory.create(AppModule); - const { httpAdapter } = app.get(HttpAdapterHost); - // eslint-disable-next-line deprecation/deprecation - Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter)); - await app.listen(port); - sendPortToRunner(port); -} - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/test.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/test.ts deleted file mode 100644 index 59532ef989da..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { conditionalTest } from '../../../utils'; -import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; - -const { TS_VERSION } = process.env; -const isOldTS = TS_VERSION && TS_VERSION.startsWith('3.'); - -// This is required to run the test with ts-node and decorators -process.env.TS_NODE_PROJECT = `${__dirname}/tsconfig.json`; - -conditionalTest({ min: 16 })('nestjs auto instrumentation', () => { - afterAll(async () => { - cleanupChildProcesses(); - }); - - test("should assign scope's transactionName if express integration is disabled", done => { - if (isOldTS) { - // Skipping test on old TypeScript - return done(); - } - - createRunner(__dirname, 'scenario.ts') - .ignore('transaction') - .expect({ - event: { - exception: { - values: [ - { - value: 'error with id 456', - }, - ], - }, - transaction: 'GET /test-exception/:id', - }, - }) - .start(done) - .makeRequest('get', '/test-exception/456'); - }); -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/tsconfig.json b/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/tsconfig.json deleted file mode 100644 index 84b8f8d6c44e..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "include": ["scenario.ts"], - "compilerOptions": { - "module": "commonjs", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "target": "ES2021", - } -} diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts deleted file mode 100644 index 449dc82fd070..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ -import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - transport: loggingTransport, -}); - -import { Controller, Get, Injectable, Module } from '@nestjs/common'; -import { BaseExceptionFilter, HttpAdapterHost, NestFactory } from '@nestjs/core'; - -const port = 3450; - -// Stop the process from exiting before the transaction is sent -// eslint-disable-next-line @typescript-eslint/no-empty-function -setInterval(() => {}, 1000); - -@Injectable() -class AppService { - getHello(): string { - return 'Hello World!'; - } -} - -@Controller() -class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} - -@Module({ - imports: [], - controllers: [AppController], - providers: [AppService], -}) -class AppModule {} - -async function run(): Promise { - const app = await NestFactory.create(AppModule); - await app.listen(port); - - const { httpAdapter } = app.get(HttpAdapterHost); - // eslint-disable-next-line deprecation/deprecation - Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter)); - sendPortToRunner(port); -} - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs/test.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs/test.ts deleted file mode 100644 index 80570044d64d..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs/test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { conditionalTest } from '../../../utils'; -import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; - -const { TS_VERSION } = process.env; -const isOldTS = TS_VERSION && TS_VERSION.startsWith('3.'); - -// This is required to run the test with ts-node and decorators -process.env.TS_NODE_PROJECT = `${__dirname}/tsconfig.json`; - -conditionalTest({ min: 16 })('nestjs auto instrumentation', () => { - afterAll(async () => { - cleanupChildProcesses(); - }); - - const CREATION_TRANSACTION = { - transaction: 'Create Nest App', - }; - - const GET_TRANSACTION = { - transaction: 'GET /', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'GET /', - data: expect.objectContaining({ - 'nestjs.callback': 'getHello', - 'nestjs.controller': 'AppController', - 'nestjs.type': 'request_context', - 'sentry.op': 'request_context.nestjs', - 'sentry.origin': 'auto.http.otel.nestjs', - component: '@nestjs/core', - 'http.method': 'GET', - 'http.route': '/', - 'http.url': '/', - }), - }), - ]), - }; - - test('should auto-instrument `nestjs` package', done => { - if (isOldTS) { - // Skipping test on old TypeScript - return done(); - } - - createRunner(__dirname, 'scenario.ts') - .expect({ transaction: CREATION_TRANSACTION }) - .expect({ transaction: GET_TRANSACTION }) - .start(done) - .makeRequest('get', '/'); - }); -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs/tsconfig.json b/dev-packages/node-integration-tests/suites/tracing/nestjs/tsconfig.json deleted file mode 100644 index 84b8f8d6c44e..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "include": ["scenario.ts"], - "compilerOptions": { - "module": "commonjs", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "target": "ES2021", - } -} diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json index b40c92b4356e..a0b24c52e319 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "engines": { - "node": ">=16" + "node": ">=18" }, "scripts": { "db-up": "docker compose up -d", @@ -16,7 +16,7 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "5.9.1", - "prisma": "^5.9.1" + "@prisma/client": "6.2.1", + "prisma": "6.2.1" } } diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma index 52682f1b6cf5..4363c97738ee 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma @@ -5,7 +5,6 @@ datasource db { generator client { provider = "prisma-client-js" - previewFeatures = ["tracing"] } model User { diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.js b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.js index 82fbc044b973..b6e3805fa595 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.js +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.js @@ -16,14 +16,14 @@ const { PrismaClient } = require('@prisma/client'); setInterval(() => {}, 1000); async function run() { - const client = new PrismaClient(); - await Sentry.startSpan( { name: 'Test Transaction', op: 'transaction', }, async span => { + const client = new PrismaClient(); + await client.user.create({ data: { name: 'Tilda', diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts deleted file mode 100755 index a0052511b380..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { execSync } from 'child_process'; -import { parseSemver } from '@sentry/core'; - -const NODE_VERSION = parseSemver(process.versions.node); - -// Prisma v5 requires Node.js v16+ -// https://www.prisma.io/docs/orm/more/upgrade-guides/upgrading-versions/upgrading-to-prisma-5#nodejs-minimum-version-change -if (NODE_VERSION.major && NODE_VERSION.major < 16) { - // eslint-disable-next-line no-console - console.warn(`Skipping Prisma tests on Node: ${NODE_VERSION.major}`); - process.exit(0); -} - -try { - execSync('yarn && yarn setup'); -} catch (_) { - process.exit(1); -} diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts index dd92de5d0292..70d2fda9cbe0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts @@ -1,105 +1,85 @@ -import { conditionalTest } from '../../../utils'; +import type { SpanJSON } from '@sentry/core'; import { createRunner } from '../../../utils/runner'; -conditionalTest({ min: 16 })('Prisma ORM Tests', () => { +describe('Prisma ORM Tests', () => { test('CJS - should instrument PostgreSQL queries from Prisma ORM', done => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - method: 'create', - model: 'User', - name: 'User.create', - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:client:operation', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:client:serialize', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:client:connect', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:engine', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'db.type': 'postgres', - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:engine:connection', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'db.statement': expect.stringContaining( - 'INSERT INTO "public"."User" ("createdAt","email","name") VALUES ($1,$2,$3) RETURNING "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" /* traceparent', - ), - 'sentry.origin': 'auto.db.otel.prisma', - 'db.system': 'prisma', - 'sentry.op': 'db', - }, - description: expect.stringContaining( - 'INSERT INTO "public"."User" ("createdAt","email","name") VALUES ($1,$2,$3) RETURNING "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" /* traceparent', - ), - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:engine:serialize', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:engine:response_json_serialization', - status: 'ok', - }), - expect.objectContaining({ - data: { - method: 'findMany', - model: 'User', - name: 'User.findMany', - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:client:operation', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:client:serialize', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:engine', - status: 'ok', - }), - ]), - }; + createRunner(__dirname, 'scenario.js') + .expect({ + transaction: transaction => { + expect(transaction.transaction).toBe('Test Transaction'); - createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); + const spans = transaction.spans || []; + expect(spans.length).toBeGreaterThanOrEqual(5); + + function expectPrismaSpanToIncludeSpanWith(span: Partial) { + expect(spans).toContainEqual( + expect.objectContaining({ + ...span, + data: { + ...span.data, + 'sentry.origin': 'auto.db.otel.prisma', + }, + status: 'ok', + }), + ); + } + + expectPrismaSpanToIncludeSpanWith({ + description: 'prisma:client:detect_platform', + }); + + expectPrismaSpanToIncludeSpanWith({ + description: 'prisma:client:load_engine', + }); + + expectPrismaSpanToIncludeSpanWith({ + description: 'prisma:client:operation', + data: { + method: 'create', + model: 'User', + name: 'User.create', + }, + }); + + expectPrismaSpanToIncludeSpanWith({ + description: 'prisma:client:serialize', + }); + + expectPrismaSpanToIncludeSpanWith({ + description: 'prisma:client:connect', + }); + + expectPrismaSpanToIncludeSpanWith({ + description: 'prisma:engine:connect', + }); + + expectPrismaSpanToIncludeSpanWith({ + description: 'prisma:engine:query', + }); + + expectPrismaSpanToIncludeSpanWith({ + data: { + 'sentry.op': 'db', + 'db.query.text': + 'SELECT "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" FROM "public"."User" WHERE 1=1 OFFSET $1', + 'db.system': 'postgresql', + 'otel.kind': 'CLIENT', + }, + description: + 'SELECT "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" FROM "public"."User" WHERE 1=1 OFFSET $1', + }); + + expectPrismaSpanToIncludeSpanWith({ + data: { + 'sentry.op': 'db', + 'db.query.text': 'DELETE FROM "public"."User" WHERE "public"."User"."email"::text LIKE $1', + 'db.system': 'postgresql', + 'otel.kind': 'CLIENT', + }, + description: 'DELETE FROM "public"."User" WHERE "public"."User"."email"::text LIKE $1', + }); + }, + }) + .start(done); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock index 9c0fc47be4be..fbdff7a9505d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock @@ -2,50 +2,57 @@ # yarn lockfile v1 -"@prisma/client@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.9.1.tgz#d92bd2f7f006e0316cb4fda9d73f235965cf2c64" - integrity sha512-caSOnG4kxcSkhqC/2ShV7rEoWwd3XrftokxJqOCMVvia4NYV/TPtJlS9C2os3Igxw/Qyxumj9GBQzcStzECvtQ== - -"@prisma/debug@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.9.1.tgz#906274e73d3267f88b69459199fa3c51cd9511a3" - integrity sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA== - -"@prisma/engines-version@5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64": - version "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64.tgz#54d2164f28d23e09d41cf9eb0bddbbe7f3aaa660" - integrity sha512-HFl7275yF0FWbdcNvcSRbbu9JCBSLMcurYwvWc8WGDnpu7APxQo2ONtZrUggU3WxLxUJ2uBX+0GOFIcJeVeOOQ== - -"@prisma/engines@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.9.1.tgz#767539afc6f193a182d0495b30b027f61f279073" - integrity sha512-gkdXmjxQ5jktxWNdDA5aZZ6R8rH74JkoKq6LD5mACSvxd2vbqWeWIOV0Py5wFC8vofOYShbt6XUeCIUmrOzOnQ== +"@prisma/client@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-6.2.1.tgz#3d7d0c8669bba490247e1ffff67b93a516bd789f" + integrity sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA== + +"@prisma/debug@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-6.2.1.tgz#887719967c4942d125262e48f6c47c45d17c1f61" + integrity sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ== + +"@prisma/engines-version@6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69": + version "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz#b84ce3fab44bfa13a22669da02752330b61745b2" + integrity sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ== + +"@prisma/engines@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-6.2.1.tgz#14ef56bb780f02871a728667161d997a14aedb69" + integrity sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ== dependencies: - "@prisma/debug" "5.9.1" - "@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" - "@prisma/fetch-engine" "5.9.1" - "@prisma/get-platform" "5.9.1" - -"@prisma/fetch-engine@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.9.1.tgz#5d3b2c9af54a242e37b3f9561b59ab72f8e92818" - integrity sha512-l0goQOMcNVOJs1kAcwqpKq3ylvkD9F04Ioe1oJoCqmz05mw22bNAKKGWuDd3zTUoUZr97va0c/UfLNru+PDmNA== + "@prisma/debug" "6.2.1" + "@prisma/engines-version" "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69" + "@prisma/fetch-engine" "6.2.1" + "@prisma/get-platform" "6.2.1" + +"@prisma/fetch-engine@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-6.2.1.tgz#cd7eb7428a407105e0f3761dba536aefd41fc7f7" + integrity sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A== dependencies: - "@prisma/debug" "5.9.1" - "@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" - "@prisma/get-platform" "5.9.1" - -"@prisma/get-platform@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.9.1.tgz#a66bb46ab4d30db786c84150ef074ab0aad4549e" - integrity sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg== + "@prisma/debug" "6.2.1" + "@prisma/engines-version" "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69" + "@prisma/get-platform" "6.2.1" + +"@prisma/get-platform@6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-6.2.1.tgz#34313cd0ee3587798ad33a7b57b6342dc8e66426" + integrity sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q== dependencies: - "@prisma/debug" "5.9.1" + "@prisma/debug" "6.2.1" -prisma@^5.9.1: - version "5.9.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.9.1.tgz#baa3dd635fbf71504980978f10f55ea11068f6aa" - integrity sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ== +fsevents@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +prisma@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-6.2.1.tgz#457b210326d66d0e6f583cc6f9cd2819b984408f" + integrity sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA== dependencies: - "@prisma/engines" "5.9.1" + "@prisma/engines" "6.2.1" + optionalDependencies: + fsevents "2.3.3" diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts index c0d783aaa594..254d197c85c3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts @@ -1,8 +1,7 @@ -import { conditionalTest } from '../../../../utils'; import { createRunner } from '../../../../utils/runner'; import { createTestServer } from '../../../../utils/server'; -conditionalTest({ min: 18 })('outgoing fetch', () => { +describe('outgoing fetch', () => { test('outgoing fetch requests create breadcrumbs', done => { createTestServer(done) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts index 9c732d899cde..906fa6541dd6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts @@ -1,8 +1,7 @@ -import { conditionalTest } from '../../../../utils'; import { createRunner } from '../../../../utils/runner'; import { createTestServer } from '../../../../utils/server'; -conditionalTest({ min: 18 })('outgoing fetch', () => { +describe('outgoing fetch', () => { test('outgoing fetch requests are correctly instrumented with tracing disabled', done => { expect.assertions(11); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts index fde1c787829a..afe60d27b22a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts @@ -1,8 +1,7 @@ -import { conditionalTest } from '../../../../utils'; import { createRunner } from '../../../../utils/runner'; import { createTestServer } from '../../../../utils/server'; -conditionalTest({ min: 18 })('outgoing fetch', () => { +describe('outgoing fetch', () => { test('outgoing sampled fetch requests without active span are correctly instrumented', done => { expect.assertions(11); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts index d288e9a03fbf..cb85ca98ca0b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts @@ -1,8 +1,7 @@ -import { conditionalTest } from '../../../../utils'; import { createRunner } from '../../../../utils/runner'; import { createTestServer } from '../../../../utils/server'; -conditionalTest({ min: 18 })('outgoing fetch', () => { +describe('outgoing fetch', () => { test('outgoing fetch requests are correctly instrumented when not sampled', done => { expect.assertions(11); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-esm/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-esm/test.ts index 72f625aedeb7..f3d58877c8f3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-esm/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-esm/test.ts @@ -1,9 +1,8 @@ import { join } from 'path'; -import { conditionalTest } from '../../../../utils'; import { createRunner } from '../../../../utils/runner'; import { createTestServer } from '../../../../utils/server'; -conditionalTest({ min: 18 })('outgoing http in ESM', () => { +describe('outgoing http in ESM', () => { test('outgoing sampled http requests are correctly instrumented in ESM', done => { expect.assertions(11); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js new file mode 100644 index 000000000000..d7b54cb25c4f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js @@ -0,0 +1,40 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + tracesSampleRate: 0.00000001, // It's important that this is not 1, so that we also check logic for NonRecordingSpans, which is usually the edge-case +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/test.ts new file mode 100644 index 000000000000..7c566c1d8eeb --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/test.ts @@ -0,0 +1,93 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('sample_rand propagation', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('propagates a sample rand when there are no incoming trace headers', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect(response).toEqual({ + propagatedData: { + baggage: expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), + }, + }); + }); + + test('propagates a sample rand when there is a sentry-trace header and incoming sentry baggage', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-release=foo,sentry-sample_rand=0.424242', + }, + }); + expect(response).toEqual({ + propagatedData: { + baggage: expect.stringMatching(/sentry-sample_rand=0\.424242/), + }, + }); + }); + + test('propagates a sample rand when there is an incoming sentry-trace header but no baggage header', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + }, + }); + expect(response).toEqual({ + propagatedData: { + baggage: expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), + }, + }); + }); + + test('propagates a sample_rand that would lead to a positive sampling decision when there is an incoming positive sampling decision but no sample_rand in the baggage header', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.25', + }, + }); + + const sampleRand = Number((response as any).propagatedData.baggage.match(/sentry-sample_rand=(0\.[0-9]+)/)[1]); + + expect(sampleRand).toStrictEqual(expect.any(Number)); + expect(sampleRand).not.toBeNaN(); + expect(sampleRand).toBeLessThan(0.25); + expect(sampleRand).toBeGreaterThanOrEqual(0); + }); + + test('propagates a sample_rand that would lead to a negative sampling decision when there is an incoming negative sampling decision but no sample_rand in the baggage header', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: 'sentry-sample_rate=0.75', + }, + }); + + const sampleRand = Number((response as any).propagatedData.baggage.match(/sentry-sample_rand=(0\.[0-9]+)/)[1]); + + expect(sampleRand).toStrictEqual(expect.any(Number)); + expect(sampleRand).not.toBeNaN(); + expect(sampleRand).toBeGreaterThanOrEqual(0.75); + expect(sampleRand).toBeLessThan(1); + }); + + test('a new sample_rand when there is no sentry-trace header but a baggage header with sample_rand', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + baggage: 'sentry-sample_rate=0.75,sentry-sample_rand=0.5', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rand=0\.[0-9]+/); + const sampleRandStr = (response as any).propagatedData.baggage.match(/sentry-sample_rand=(0\.[0-9]+)/)[1]; + expect(sampleRandStr).not.toBe('0.5'); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js new file mode 100644 index 000000000000..dbc6b9009c49 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js @@ -0,0 +1,39 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts new file mode 100644 index 000000000000..c6800055c84b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts @@ -0,0 +1,25 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with no tracing enabled', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate an incoming sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should not propagate a sample rate for root traces', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate/); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js new file mode 100644 index 000000000000..dc2f49b081cf --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js @@ -0,0 +1,40 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + tracesSampleRate: 0, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts new file mode 100644 index 000000000000..c12d2920dd9f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts @@ -0,0 +1,61 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with tracesSampleRate=0', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate incoming sample rate when inheriting a positive sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should propagate incoming sample rate when inheriting a negative sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should propagate configured sample rate when receiving a trace without sampling decision and sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/); + }); + + test('should propagate configured sample rate when receiving a trace without sampling decision, but with sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/); + }); + + test('should propagate configured sample rate when there is no incoming trace', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js new file mode 100644 index 000000000000..512681043d4d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js @@ -0,0 +1,40 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + tracesSampleRate: 0.69, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts new file mode 100644 index 000000000000..27afa03e9045 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts @@ -0,0 +1,61 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with tracesSampleRate', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate incoming sample rate when inheriting a positive sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should propagate incoming sample rate when inheriting a negative sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should propagate configured sample rate when receiving a trace without sampling decision and sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); + }); + + test('should propagate configured sample rate when receiving a trace without sampling decision, but with sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); + }); + + test('should propagate configured sample rate when there is no incoming trace', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js new file mode 100644 index 000000000000..5dc1d17588e5 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js @@ -0,0 +1,46 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + tracesSampler: ({ parentSampleRate }) => { + if (parentSampleRate) { + return parentSampleRate; + } + + return 0.69; + }, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts new file mode 100644 index 000000000000..304725268f03 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts @@ -0,0 +1,37 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with tracesSampler', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming trace', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); + }); + + test('should propagate sample_rate equivalent to incoming sample_rate (because tracesSampler is configured that way)', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts b/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts index c4a0ae29fe38..6c5fd17f833a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts @@ -1,11 +1,10 @@ -import { conditionalTest } from '../../../utils'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; jest.setTimeout(75000); // Tedious version we are testing against only supports Node 18+ // https://github.com/tediousjs/tedious/blob/8310c455a2cc1cba83c1ca3c16677da4f83e12a9/package.json#L38 -conditionalTest({ min: 18 })('tedious auto instrumentation', () => { +describe('tedious auto instrumentation', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/test.txt b/dev-packages/node-integration-tests/test.txt new file mode 100644 index 000000000000..0a0fa7f94de9 --- /dev/null +++ b/dev-packages/node-integration-tests/test.txt @@ -0,0 +1,213 @@ +yarn run v1.22.22 +$ /Users/abhijeetprasad/workspace/sentry-javascript/node_modules/.bin/jest contextLines/memory-leak + console.log + starting scenario /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts [ '-r', 'ts-node/register' ] undefined + + at log (utils/runner.ts:462:11) + + console.log + line COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad cwd DIR 1,16 608 107673020 /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad txt REG 1,16 88074480 114479727 /Users/abhijeetprasad/.volta/tools/image/node/18.20.5/bin/node + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 0u unix 0x6a083c8cc83ea8db 0t0 ->0xf2cacdd1d3a0ebec + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 1u unix 0xd99cc422a76ba47f 0t0 ->0x542148981a0b9ef2 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 2u unix 0x97e70527ed5803f8 0t0 ->0xbafdaf00ef20de83 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 3u KQUEUE count=0, state=0 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 4 PIPE 0x271836c29e42bc67 16384 ->0x16ac23fcfd4fe1a3 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 5 PIPE 0x16ac23fcfd4fe1a3 16384 ->0x271836c29e42bc67 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 6 PIPE 0xd76fcd4ca2a35fcf 16384 ->0x30d26cd4f0e069b2 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 7 PIPE 0x30d26cd4f0e069b2 16384 ->0xd76fcd4ca2a35fcf + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 8 PIPE 0x37691847717c3d6 16384 ->0x966eedd79d018252 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 9 PIPE 0x966eedd79d018252 16384 ->0x37691847717c3d6 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 10u KQUEUE count=0, state=0xa + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 11 PIPE 0x99c1186f14b865be 16384 ->0xe88675eb1eefb2b + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 12 PIPE 0xe88675eb1eefb2b 16384 ->0x99c1186f14b865be + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 13 PIPE 0x52173210451cdda9 16384 ->0x50bbc31a0f1cc1af + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 14 PIPE 0x50bbc31a0f1cc1af 16384 ->0x52173210451cdda9 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 15u KQUEUE count=0, state=0 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 16 PIPE 0xa115aa0653327e72 16384 ->0x100525c465ee1eb0 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 17 PIPE 0x100525c465ee1eb0 16384 ->0xa115aa0653327e72 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 18 PIPE 0x41945cf9fe740277 16384 ->0x8791d18eade5b1e0 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 19 PIPE 0x8791d18eade5b1e0 16384 ->0x41945cf9fe740277 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 20r CHR 3,2 0t0 333 /dev/null + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 21u KQUEUE count=0, state=0xa + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 22 PIPE 0xf4c6a2f47fb0bff5 16384 ->0xa00185e1c59cedbe + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 23 PIPE 0xa00185e1c59cedbe 16384 ->0xf4c6a2f47fb0bff5 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 24 PIPE 0x4ac25a99f45f7ca4 16384 ->0x2032aef840c94700 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 25 PIPE 0x2032aef840c94700 16384 ->0x4ac25a99f45f7ca4 + + at log (utils/runner.ts:462:11) + + console.log + line null + + at log (utils/runner.ts:462:11) + + console.log + line [{"sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"}},[[{"type":"session"},{"sid":"0ae9ef2ac2ba49dd92b6dab9d81444ac","init":true,"started":"2025-01-13T21:47:47.502Z","timestamp":"2025-01-13T21:47:47.663Z","status":"ok","errors":1,"duration":0.16146087646484375,"attrs":{"release":"1.0","environment":"production"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"2626269e3c634fc289338c441e76412c","sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 0","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"2626269e3c634fc289338c441e76412c","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b1e1b8a0d410ef14"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.528,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"f58236bf0a7f4a999f7daf5283f0400f","sent_at":"2025-01-13T21:47:47.664Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 1","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"f58236bf0a7f4a999f7daf5283f0400f","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9b6ccaf59536bcb4"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.531,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 2","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"82d56f443d3f01f9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.532,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"293d7c8c731c48eca30735b41efd40ba","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 3","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"293d7c8c731c48eca30735b41efd40ba","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8be46494d3555ddb"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"e9273b56624d4261b00f5431852da167","sent_at":"2025-01-13T21:47:47.666Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 4","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"e9273b56624d4261b00f5431852da167","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9a067a8906c8c147"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 5","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"ac2ad9041812f9d9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.534,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"65224267e02049daadbc577de86960f3","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 6","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"65224267e02049daadbc577de86960f3","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b12818330e05cd2f"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.535,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 7","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"83cb86896d96bbf6"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 8","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"a0e8e199fcf05714"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"dc08b3fe26e94759817c7b5e95469727","sent_at":"2025-01-13T21:47:47.669Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 9","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"dc08b3fe26e94759817c7b5e95469727","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8ec7d145c5362df0"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270106624},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.537,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + +Done in 4.21s. diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index bc4fb901e2db..a5fc8df38825 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -152,7 +152,7 @@ export function createRunner(...paths: string[]) { let expectedEnvelopeHeaders: ExpectedEnvelopeHeader[] | undefined = undefined; const flags: string[] = []; // By default, we ignore session & sessions - const ignored: Set = new Set(['session', 'sessions']); + const ignored: Set = new Set(['session', 'sessions', 'client_report']); let withEnv: Record = {}; let withSentryServer = false; let dockerOptions: DockerOptions | undefined; @@ -168,6 +168,12 @@ export function createRunner(...paths: string[]) { expectedEnvelopes.push(expected); return this; }, + expectN: function (n: number, expected: Expected) { + for (let i = 0; i < n; i++) { + expectedEnvelopes.push(expected); + } + return this; + }, expectHeader: function (expected: ExpectedEnvelopeHeader) { if (!expectedEnvelopeHeaders) { expectedEnvelopeHeaders = []; diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 4e6483364ee4..cf9b723599dd 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -15,7 +15,6 @@ import { defineConfig } from 'rollup'; import { makeCleanupPlugin, makeDebugBuildStatementReplacePlugin, - makeExtractPolyfillsPlugin, makeImportMetaUrlReplacePlugin, makeNodeResolvePlugin, makeRrwebBuildPlugin, @@ -31,20 +30,17 @@ const packageDotJSON = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), '. export function makeBaseNPMConfig(options = {}) { const { entrypoints = ['src/index.ts'], - esModuleInterop = false, hasBundles = false, packageSpecificConfig = {}, - addPolyfills = true, sucrase = {}, bundledBuiltins = [], } = options; const nodeResolvePlugin = makeNodeResolvePlugin(); - const sucrasePlugin = makeSucrasePlugin({}, { disableESTransforms: !addPolyfills, ...sucrase }); + const sucrasePlugin = makeSucrasePlugin({}, sucrase); const debugBuildStatementReplacePlugin = makeDebugBuildStatementReplacePlugin(); const importMetaUrlReplacePlugin = makeImportMetaUrlReplacePlugin(); const cleanupPlugin = makeCleanupPlugin(); - const extractPolyfillsPlugin = makeExtractPolyfillsPlugin(); const rrwebBuildPlugin = makeRrwebBuildPlugin({ excludeShadowDom: undefined, excludeIframe: undefined, @@ -59,20 +55,15 @@ 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, + // Include __esModule property when there is a default prop + esModule: 'if-default-prop', // output individual files rather than one big bundle preserveModules: true, - // Allow wrappers or helper functions generated by rollup to use any ES2015 features except symbols. (Symbols in - // general are fine, but the `[Symbol.toStringTag]: 'Module'` which Rollup adds alongside `__esModule: - // true` in CJS modules makes it so that Jest <= 29.2.2 crashes when trying to mock generated `@sentry/xxx` - // packages. See https://github.com/getsentry/sentry-javascript/pull/6043.) + // Allow wrappers or helper functions generated by rollup to use any ES2015 features generatedCode: { preset: 'es2015', - symbols: false, }, // don't add `"use strict"` to the top of cjs files @@ -91,16 +82,7 @@ export function makeBaseNPMConfig(options = {}) { // (We don't need it, so why waste the bytes?) freeze: false, - // Equivalent to `esModuleInterop` in tsconfig. - // Controls whether rollup emits helpers to handle special cases where turning - // `import * as dogs from 'dogs'` - // into - // `const dogs = require('dogs')` - // doesn't work. - // - // `auto` -> emit helpers - // `esModule` -> don't emit helpers - interop: esModuleInterop ? 'auto' : 'esModule', + interop: 'esModule', }, plugins: [ @@ -121,10 +103,6 @@ export function makeBaseNPMConfig(options = {}) { ], }; - if (addPolyfills) { - defaultBaseConfig.plugins.push(extractPolyfillsPlugin); - } - return deepMerge(defaultBaseConfig, packageSpecificConfig, { // Plugins have to be in the correct order or everything breaks, so when merging we have to manually re-order them customMerge: key => (key === 'plugins' ? mergePlugins : undefined), @@ -132,9 +110,13 @@ export function makeBaseNPMConfig(options = {}) { } export function makeNPMConfigVariants(baseConfig, options = {}) { - const { emitEsm = true } = options; + const { emitEsm = true, emitCjs = true } = options; - const variantSpecificConfigs = [{ output: { format: 'cjs', dir: path.join(baseConfig.output.dir, 'cjs') } }]; + const variantSpecificConfigs = []; + + if (emitCjs) { + variantSpecificConfigs.push({ output: { format: 'cjs', dir: path.join(baseConfig.output.dir, 'cjs') } }); + } if (emitEsm) { variantSpecificConfigs.push({ diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index dce0ca15bf35..9d6edd3157c0 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -126,8 +126,6 @@ export function makeTerserPlugin() { '_sentryId', // Keeps the frozen DSC on a Sentry Span '_frozenDsc', - // This keeps metrics summary on spans - '_metrics_summary', // These are used to keep span & scope relationships '_sentryRootSpan', '_sentryChildSpans', diff --git a/dev-packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs b/dev-packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs deleted file mode 100644 index e0a21b400f35..000000000000 --- a/dev-packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs +++ /dev/null @@ -1,214 +0,0 @@ -import * as path from 'path'; - -import * as acorn from 'acorn'; -import * as recast from 'recast'; - -const POLYFILL_NAMES = new Set([ - '_asyncNullishCoalesce', - '_asyncOptionalChain', - '_asyncOptionalChainDelete', - '_nullishCoalesce', - '_optionalChain', - '_optionalChainDelete', -]); - -/** - * Create a plugin which will replace function definitions of any of the above functions with an `import` or `require` - * statement pulling them in from a central source. Mimics tsc's `importHelpers` option. - */ -export function makeExtractPolyfillsPlugin() { - let moduleFormat; - - // For more on the hooks used in this plugin, see https://rollupjs.org/guide/en/#output-generation-hooks - return { - name: 'extractPolyfills', - - // Figure out which build we're currently in (esm or cjs) - outputOptions(options) { - moduleFormat = options.format; - }, - - // This runs after both the sucrase transpilation (which happens in the `transform` hook) and rollup's own - // esm-i-fying or cjs-i-fying work (which happens right before `renderChunk`), in other words, after all polyfills - // will have been injected - renderChunk(code, chunk) { - const sourceFile = chunk.fileName; - - // We don't want to pull the function definitions out of their actual sourcefiles, just the places where they've - // been injected - if (sourceFile.includes('buildPolyfills')) { - return null; - } - - // The index.js file of the core package will include identifiers named after polyfills so we would inject the - // polyfills, however that would override the exports so we should just skip that file. - const isCorePackage = process.cwd().endsWith(`packages${path.sep}core`); - if (isCorePackage && sourceFile === 'index.js') { - return null; - } - - const parserOptions = { - sourceFileName: sourceFile, - // We supply a custom parser which wraps the provided `acorn` parser in order to override the `ecmaVersion` value. - // See https://github.com/benjamn/recast/issues/578. - parser: { - parse(source, options) { - return acorn.parse(source, { - ...options, - // By this point in the build, everything should already have been down-compiled to whatever JS version - // we're targeting. Setting this parser to `latest` just means that whatever that version is (or changes - // to in the future), this parser will be able to handle the generated code. - ecmaVersion: 'latest', - }); - }, - }, - }; - - const ast = recast.parse(code, parserOptions); - - // Find function definitions and function expressions whose identifiers match a known polyfill name - const polyfillNodes = findPolyfillNodes(ast); - - if (polyfillNodes.length === 0) { - return null; - } - - console.log(`${sourceFile} - polyfills: ${polyfillNodes.map(node => node.name)}`); - - // Depending on the output format, generate `import { x, y, z } from '...'` or `var { x, y, z } = require('...')` - const importOrRequireNode = createImportOrRequireNode(polyfillNodes, sourceFile, moduleFormat); - - // Insert our new `import` or `require` node at the top of the file, and then delete the function definitions it's - // meant to replace (polyfill nodes get marked for deletion in `findPolyfillNodes`) - ast.program.body = [importOrRequireNode, ...ast.program.body.filter(node => !node.shouldDelete)]; - - // In spite of the name, this doesn't actually print anything - it just stringifies the code, and keeps track of - // where original nodes end up in order to generate a sourcemap. - const result = recast.print(ast, { - sourceMapName: `${sourceFile}.map`, - quote: 'single', - }); - - return { code: result.code, map: result.map }; - }, - }; -} - -/** - * Extract the function name, regardless of the format in which the function is declared - */ -function getNodeName(node) { - // Function expressions and functions pulled from objects - if (node.type === 'VariableDeclaration') { - // In practice sucrase and rollup only ever declare one polyfill at a time, so it's safe to just grab the first - // entry here - const declarationId = node.declarations[0].id; - - // Note: Sucrase and rollup seem to only use the first type of variable declaration for their polyfills, but good to - // cover our bases - - // Declarations of the form - // `const dogs = function() { return "are great"; };` - // or - // `const dogs = () => "are great"; - if (declarationId.type === 'Identifier') { - return declarationId.name; - } - // Declarations of the form - // `const { dogs } = { dogs: function() { return "are great"; } }` - // or - // `const { dogs } = { dogs: () => "are great" }` - else if (declarationId.type === 'ObjectPattern') { - return declarationId.properties[0].key.name; - } - // Any other format - else { - return 'unknown variable'; - } - } - - // Regular old functions, of the form - // `function dogs() { return "are great"; }` - else if (node.type === 'FunctionDeclaration') { - return node.id.name; - } - - // If we get here, this isn't a node we're interested in, so just return a string we know will never match any of the - // polyfill names - else { - return 'nope'; - } -} - -/** - * Find all nodes whose identifiers match a known polyfill name. - * - * Note: In theory, this could yield false positives, if any of the magic names were assigned to something other than a - * polyfill function, but the chances of that are slim. Also, it only searches the module global scope, but that's - * always where the polyfills appear, so no reason to traverse the whole tree. - */ -function findPolyfillNodes(ast) { - const isPolyfillNode = node => { - const nodeName = getNodeName(node); - if (POLYFILL_NAMES.has(nodeName)) { - // Mark this node for later deletion, since we're going to replace it with an import statement - node.shouldDelete = true; - // Store the name in a consistent spot, regardless of node type - node.name = nodeName; - - return true; - } - - return false; - }; - - return ast.program.body.filter(isPolyfillNode); -} - -/** - * Create a node representing an `import` or `require` statement of the form - * - * import { < polyfills > } from '...' - * or - * var { < polyfills > } = require('...') - * - * @param polyfillNodes The nodes from the current version of the code, defining the polyfill functions - * @param currentSourceFile The path, relative to `src/`, of the file currently being transpiled - * @param moduleFormat Either 'cjs' or 'esm' - * @returns A single node which can be subbed in for the polyfill definition nodes - */ -function createImportOrRequireNode(polyfillNodes, currentSourceFile, moduleFormat) { - const { - callExpression, - identifier, - importDeclaration, - importSpecifier, - literal, - objectPattern, - property, - variableDeclaration, - variableDeclarator, - } = recast.types.builders; - - // Since our polyfills live in `@sentry/core`, if we're importing or requiring them there the path will have to be - // relative - const isCorePackage = process.cwd().endsWith(path.join('packages', 'core')); - const importSource = literal( - isCorePackage ? `.${path.sep}${path.relative(path.dirname(currentSourceFile), 'buildPolyfills')}` : '@sentry/core', - ); - - // This is the `x, y, z` of inside of `import { x, y, z }` or `var { x, y, z }` - const importees = polyfillNodes.map(({ name: fnName }) => - moduleFormat === 'esm' - ? importSpecifier(identifier(fnName)) - : property.from({ kind: 'init', key: identifier(fnName), value: identifier(fnName), shorthand: true }), - ); - - const requireFn = identifier('require'); - - return moduleFormat === 'esm' - ? importDeclaration(importees, importSource) - : variableDeclaration('var', [ - variableDeclarator(objectPattern(importees), callExpression(requireFn, [importSource])), - ]); -} diff --git a/dev-packages/rollup-utils/plugins/npmPlugins.mjs b/dev-packages/rollup-utils/plugins/npmPlugins.mjs index 6597e2244ab8..f29bded61f73 100644 --- a/dev-packages/rollup-utils/plugins/npmPlugins.mjs +++ b/dev-packages/rollup-utils/plugins/npmPlugins.mjs @@ -29,6 +29,10 @@ export function makeSucrasePlugin(options = {}, sucraseOptions = {}) { }, { transforms: ['typescript', 'jsx'], + // We use a custom forked version of sucrase, + // where there is a new option `disableES2019Transforms` + disableESTransforms: false, + disableES2019Transforms: true, ...sucraseOptions, }, ); @@ -173,5 +177,3 @@ export function makeCodeCovPlugin() { uploadToken: process.env.CODECOV_TOKEN, }); } - -export { makeExtractPolyfillsPlugin } from './extractPolyfillsPlugin.mjs'; diff --git a/dev-packages/test-utils/.eslintrc.js b/dev-packages/test-utils/.eslintrc.js index 98318aea5c41..fdb9952bae52 100644 --- a/dev-packages/test-utils/.eslintrc.js +++ b/dev-packages/test-utils/.eslintrc.js @@ -3,12 +3,4 @@ module.exports = { node: true, }, extends: ['../../.eslintrc.js'], - overrides: [ - { - files: ['**/*.ts'], - rules: { - '@sentry-internal/sdk/no-optional-chaining': 'off', - }, - }, - ], }; diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 09ad4cf5a55d..8e8afec9f698 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -28,7 +28,7 @@ }, "sideEffects": false, "engines": { - "node": ">=14.18" + "node": ">=18" }, "scripts": { "fix": "eslint . --format stylish --fix", diff --git a/docs/assets/run-release-workflow.png b/docs/assets/run-release-workflow.png new file mode 100644 index 000000000000..50af8d111fe8 Binary files /dev/null and b/docs/assets/run-release-workflow.png differ diff --git a/docs/changelog/v7.md b/docs/changelog/v7.md index e784702015e0..cef925871efa 100644 --- a/docs/changelog/v7.md +++ b/docs/changelog/v7.md @@ -3,13 +3,49 @@ Support for Sentry SDK v7 will be dropped soon. We recommend migrating to the latest version of the SDK. You can migrate from `v7` of the SDK to `v8` by following the [migration guide](../../MIGRATION.md#upgrading-from-7x-to-8x). +## 7.120.3 + +- fix(v7/publish): Ensure discontinued packages are published with `latest` tag (#14926) + +## 7.120.2 + +- fix(tracing-internal): Fix case when lrp keys offset is 0 (#14615) + +Work in this release contributed by @LubomirIgonda1. Thank you for your contribution! + +## 7.120.1 + +- fix(v7/cdn): Ensure `_sentryModuleMetadata` is not mangled (#14357) + +Work in this release contributed by @gilisho. Thank you for your contribution! + +## 7.120.0 + +- feat(v7/browser): Add moduleMetadataIntegration lazy loading support (#13822) + +Work in this release contributed by @gilisho. Thank you for your contribution! + +## 7.119.2 + +- chore(nextjs/v7): Bump rollup to 2.79.2 + +## 7.119.1 + +- fix(browser/v7): Ensure wrap() only returns functions (#13838 backport) + +Work in this release contributed by @legobeat. Thank you for your contribution! + +## 7.119.0 + +- backport(tracing): Report dropped spans for transactions (#13343) + ## 7.118.0 - fix(v7/bundle): Ensure CDN bundles do not overwrite `window.Sentry` (#12579) ## 7.117.0 -- feat(browser/v7): Publish browserprofling CDN bundle (#12224) +- feat(browser/v7): Publish browser profiling CDN bundle (#12224) - fix(v7/publish): Add `v7` tag to `@sentry/replay` (#12304) ## 7.116.0 diff --git a/docs/commit-issue-pr-guidelines.md b/docs/commit-issue-pr-guidelines.md index c9e7b2dd09b9..8e97cf1acd49 100644 --- a/docs/commit-issue-pr-guidelines.md +++ b/docs/commit-issue-pr-guidelines.md @@ -34,6 +34,15 @@ and committed as such onto `develop`. Please note that we cannot _enforce_ Squash Merge due to the usage of Gitflow (see below). Github remembers the last used merge method, so you'll need to make sure to double check that you are using "Squash and Merge" correctly. +## Backporting PRs/Commits + +If you want to backport a commit to a previous major version, make sure to reflect this in the PR/commit title. +The name should have the backported major as a scope prefix. For example: + +``` +feat(v8/core): Set custom transaction source for event processors (#5722) +``` + ## Gitflow We use [Gitflow](https://docs.github.com/en/get-started/quickstart/github-flow) as a branching model. diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md deleted file mode 100644 index acf268c81ef4..000000000000 --- a/docs/migration/draft-v9-migration-guide.md +++ /dev/null @@ -1,138 +0,0 @@ - - -# Deprecations - -## General - -- **Returning `null` from `beforeSendSpan` span is deprecated.** -- **Passing `undefined` to `tracesSampleRate` / `tracesSampler` / `enableTracing` will be handled differently in v9** - - In v8, a setup like the following: - - ```ts - Sentry.init({ - tracesSampleRate: undefined, - }); - ``` - - Will result in tracing being _enabled_, although no spans will be generated. - In v9, we will streamline this behavior so that passing `undefined` will result in tracing being disabled, the same as not passing the option at all. - If you are relying on `undefined` being passed in and having tracing enabled because of this, you should update your config to set e.g. `tracesSampleRate: 0` instead, which will also enable tracing in v9. - -- **The `autoSessionTracking` option is deprecated.** - - To enable session tracking, it is recommended to unset `autoSessionTracking` and ensure that either, in browser environments the `browserSessionIntegration` is added, or in server environments the `httpIntegration` is added. - To disable session tracking, it is recommended unset `autoSessionTracking` and to remove the `browserSessionIntegration` in browser environments, or in server environments configure the `httpIntegration` with the `trackIncomingRequestsAsSessions` option set to `false`. - -## `@sentry/utils` - -- **The `@sentry/utils` package has been deprecated. Import everything from `@sentry/core` instead.** - -- Deprecated `AddRequestDataToEventOptions.transaction`. This option effectively doesn't do anything anymore, and will - be removed in v9. -- Deprecated `TransactionNamingScheme` type. -- Deprecated `validSeverityLevels`. Will not be replaced. -- Deprecated `urlEncode`. No replacements. -- Deprecated `addRequestDataToEvent`. Use `addNormalizedRequestDataToEvent` instead. -- Deprecated `extractRequestData`. Instead manually extract relevant data off request. -- Deprecated `arrayify`. No replacements. -- Deprecated `memoBuilder`. No replacements. -- Deprecated `getNumberOfUrlSegments`. No replacements. -- Deprecated `BAGGAGE_HEADER_NAME`. No replacements. -- Deprecated `makeFifoCache`. No replacements. -- Deprecated `dynamicRequire`. No replacements. -- Deprecated `flatten`. No replacements. -- Deprecated `_browserPerformanceTimeOriginMode`. No replacements. - -## `@sentry/core` - -- Deprecated `transactionNamingScheme` option in `requestDataIntegration`. -- Deprecated `debugIntegration`. To log outgoing events, use [Hook Options](https://docs.sentry.io/platforms/javascript/configuration/options/#hooks) (`beforeSend`, `beforeSendTransaction`, ...). -- Deprecated `sessionTimingIntegration`. To capture session durations alongside events, use [Context](https://docs.sentry.io/platforms/javascript/enriching-events/context/) (`Sentry.setContext()`). -- Deprecated `addTracingHeadersToFetchRequest` method - this was only meant for internal use and is not needed anymore. -- Deprecated `generatePropagationContext()` in favor of using `generateTraceId()` directly. -- Deprecated `spanId` field on `propagationContext` - this field will be removed in v9, and should neither be read or set anymore. -- Deprecated `RequestSession` type. No replacements. -- Deprecated `RequestSessionStatus` type. No replacements. -- Deprecated `SessionFlusherLike` type. No replacements. -- Deprecated `SessionFlusher`. No replacements. - -## `@sentry/nestjs` - -- Deprecated `@WithSentry`. Use `@SentryExceptionCaptured` instead. -- Deprecated `SentryTracingInterceptor`. - If you are using `@sentry/nestjs` you can safely remove any references to the `SentryTracingInterceptor`. - If you are using another package migrate to `@sentry/nestjs` and remove the `SentryTracingInterceptor` afterwards. -- Deprecated `SentryService`. - If you are using `@sentry/nestjs` you can safely remove any references to the `SentryService`. - If you are using another package migrate to `@sentry/nestjs` and remove the `SentryService` afterwards. -- Deprecated `SentryGlobalGenericFilter`. - Use the `SentryGlobalFilter` instead. - The `SentryGlobalFilter` is a drop-in replacement. -- Deprecated `SentryGlobalGraphQLFilter`. - Use the `SentryGlobalFilter` instead. - The `SentryGlobalFilter` is a drop-in replacement. - -## `@sentry/types` - -- **The `@sentry/types` package has been deprecated. Import everything from `@sentry/core` instead.** - -- Deprecated `Request` in favor of `RequestEventData`. -- Deprecated `RequestSession`. No replacements. -- Deprecated `RequestSessionStatus`. No replacements. -- Deprecated `SessionFlusherLike`. No replacements. - -## `@sentry/nuxt` - -- Deprecated `tracingOptions` in `Sentry.init()` in favor of passing the `vueIntegration()` to `Sentry.init({ integrations: [...] })` and setting `tracingOptions` there. - -## `@sentry/vue` - -- Deprecated `tracingOptions`, `trackComponents`, `timeout`, `hooks` options everywhere other than in the `tracingOptions` option of the `vueIntegration()`. - These options should now be set as follows: - - ```ts - import * as Sentry from '@sentry/vue'; - - Sentry.init({ - integrations: [ - Sentry.vueIntegration({ - tracingOptions: { - trackComponents: true, - timeout: 1000, - hooks: ['mount', 'update', 'unmount'], - }, - }), - ], - }); - ``` - -## `@sentry/astro` - -- Deprecated passing `dsn`, `release`, `environment`, `sampleRate`, `tracesSampleRate`, `replaysSessionSampleRate` to the integration. Use the runtime-specific `Sentry.init()` calls for passing these options instead. - -## `@sentry/remix` - -- Deprecated `autoInstrumentRemix: false`. The next major version will default to behaving as if this option were `true` and the option itself will be removed. - -## `@sentry/react` - -- Deprecated `wrapUseRoutes`. Use `wrapUseRoutesV6` or `wrapUseRoutesV7` instead. -- Deprecated `wrapCreateBrowserRouter`. Use `wrapCreateBrowserRouterV6` or `wrapCreateBrowserRouterV7` instead. - -## `@sentry/nextjs` - -- Deprecated `hideSourceMaps`. No replacements. The SDK emits hidden sourcemaps by default. - -## `@sentry/opentelemetry` - -- Deprecated `generateSpanContextForPropagationContext` in favor of doing this manually - we do not need this export anymore. - -## Server-side SDKs (`@sentry/node` and all dependents) - -- Deprecated `processThreadBreadcrumbIntegration` in favor of `childProcessIntegration`. Functionally they are the same. -- Deprecated `nestIntegration`. Use the NestJS SDK (`@sentry/nestjs`) instead. -- Deprecated `setupNestErrorHandler`. Use the NestJS SDK (`@sentry/nestjs`) instead. -- Deprecated `addOpenTelemetryInstrumentation`. Use the `openTelemetryInstrumentations` option in `Sentry.init()` or your custom Sentry Client instead. -- Deprecated `registerEsmLoaderHooks.include` and `registerEsmLoaderHooks.exclude`. Set `onlyIncludeInstrumentedModules: true` instead. -- `registerEsmLoaderHooks` will only accept `true | false | undefined` in the future. The SDK will default to wrapping modules that are used as part of OpenTelemetry Instrumentation. diff --git a/docs/migration/v8-to-v9.md b/docs/migration/v8-to-v9.md new file mode 100644 index 000000000000..a6bd0aa35e7c --- /dev/null +++ b/docs/migration/v8-to-v9.md @@ -0,0 +1,538 @@ +# Upgrading from 8.x to 9.x + +**DISCLAIMER: THIS MIGRATION GUIDE IS WORK IN PROGRESS** + +Version 9 of the Sentry SDK concerns API cleanup and compatibility updates. +This update contains behavioral changes that will not be caught by TypeScript or linters, so we recommend carefully reading the section on [Behavioral Changes](#2-behavior-changes). + +Before updating to version `9.x` of the SDK, we recommend upgrading to the latest version of `8.x`. +You can then go through the [Deprecations in 8.x](#deprecations-in-8x) and remove and migrate usages of deprecated APIs in your code before upgrading to `9.x`. + +Version 9 of the JavaScript SDK is compatible with Sentry self-hosted versions 24.4.2 or higher (unchanged from last major). +Lower versions may continue to work, but may not support all features. + +## 1. Version Support Changes: + +Version 9 of the Sentry SDK has new compatibility ranges for runtimes and frameworks. +We periodically update the compatibility ranges in major versions to increase the reliability and quality of APIs and instrumentation data. + +### General Runtime Support Changes + +**ECMAScript Version:** All the JavaScript code in the Sentry SDK packages may now contain ECMAScript 2020 features. +This includes features like Nullish Coalescing (`??`), Optional Chaining (`?.`), `String.matchAll()`, Logical Assignment Operators (`&&=`, `||=`, `??=`), and `Promise.allSettled()`. + +If you observe failures due to syntax or features listed above, it may indicate that your current runtime does not support ES2020. +If your runtime does not support ES2020, we recommend transpiling the SDK using Babel or similar tooling. + +**Node.js:** The minimum supported Node.js version is **18.0.0**, except for ESM-only SDKs (nuxt, solidstart, astro) which require Node **18.19.1** or up. +We no longer test against Node 14 and Node 16 and cannot guarantee that the SDK will work as expected on these versions. + +**Browsers:** Due to SDK code now including ES2020 features, the minimum supported browser list now looks as follows: + +- Chrome 80 +- Edge 80 +- Safari 14, iOS Safari 14.4 +- Firefox 74 +- Opera 67 +- Samsung Internet 13.0 + +If you need to support older browsers, we recommend transpiling your code using Babel or similar tooling. + +**Deno:** The minimum supported Deno version is now **2.0.0**. + +### Framework and Library Support Changes + +Support for the following frameworks and library versions are dropped: + +- **Remix**: Version `1.x` +- **TanStack Router**: Version `1.63.0` and lower (relevant when using `tanstackRouterBrowserTracingIntegration`) +- **SvelteKit**: Version `1.x` +- **Ember.js**: Version `3.x` and lower (minimum supported version is `4.x`) +- **Prisma**: Version `5.x` + +### TypeScript Version Policy + +In preparation for the OpenTelemetry SDK v2, which will raise the minimum required TypeScript version, the minimum required TypeScript version is increased to version `5.0.4` (TBD https://github.com/open-telemetry/opentelemetry-js/pull/5145). + +Additionally, like the OpenTelemetry SDK, the Sentry JavaScript SDK will follow [DefinitelyType's version support policy](https://github.com/DefinitelyTyped/DefinitelyTyped#support-window) which has a support time frame of 2 years for any released version of TypeScript. + +Older Typescript versions _may_ still work, but we will not test them anymore and no more guarantees apply. + +## 2. Behavior Changes + +### `@sentry/core` / All SDKs + +- If you use the optional `captureConsoleIntegration` and set `attachStackTrace: true` in your `Sentry.init` call, console messages will no longer be marked as unhandled (i.e. `handled: false`) but as handled (i.e. `handled: true`). If you want to keep sending them as unhandled, configure the `handled` option when adding the integration: + + ```js + Sentry.init({ + integrations: [Sentry.captureConsoleIntegration({ handled: false })], + attachStackTrace: true, + }); + ``` + +- Dropping spans in the `beforeSendSpan` hook is no longer possible. +- The `beforeSendSpan` hook now receives the root span as well as the child spans. +- In previous versions, we determined if tracing is enabled (for Tracing Without Performance) by checking if either `tracesSampleRate` or `traceSampler` are _defined_ at all, in `Sentry.init()`. This means that e.g. the following config would lead to tracing without performance (=tracing being enabled, even if no spans would be started): + + ```js + Sentry.init({ + tracesSampleRate: undefined, + }); + ``` + + In v9, an `undefined` value will be treated the same as if the value is not defined at all. You'll need to set `tracesSampleRate: 0` if you want to enable tracing without performance. + +- The `getCurrentHub().getIntegration(IntegrationClass)` method will always return `null` in v9. This has already stopped working mostly in v8, because we stopped exposing integration classes. In v9, the fallback behavior has been removed. Note that this does not change the type signature and is thus not technically breaking, but still worth pointing out. + +- The `startSpan` behavior was slightly changed if you pass a custom `scope` to the span start options: While in v8, the passed scope was set active directly on the passed scope, in v9, the scope is cloned. This behavior change does not apply to `@sentry/node` where the scope was already cloned. This change was made to ensure that the span only remains active within the callback and to align behavior between `@sentry/node` and all other SDKs. As a result of the change, your span hierarchy should be more accurate. However, be aware that modifying the scope (e.g. set tags) within the `startSpan` callback behaves a bit differently now. + + ```js + startSpan({ name: 'example', scope: customScope }, () => { + getCurrentScope().setTag('tag-a', 'a'); // this tag will only remain within the callback + // set the tag directly on customScope in addition, if you want to to persist the tag outside of the callback + customScope.setTag('tag-a', 'a'); + }); + ``` + +### `@sentry/node` + +- When `skipOpenTelemetrySetup: true` is configured, `httpIntegration({ spans: false })` will be configured by default. + + This means that you no longer have to specify this yourself in this scenario. With this change, no spans are emitted once `skipOpenTelemetrySetup: true` is configured, without any further configuration being needed. + +- The `requestDataIntegration` will no longer automatically set the user from `request.user`. This is an express-specific, undocumented behavior, and also conflicts with our privacy-by-default strategy. Starting in v9, you'll need to manually call `Sentry.setUser()` e.g. in a middleware to set the user on Sentry events. + +- The `tracesSampler` hook will no longer be called for _every_ span. Instead, it will only be called for "root spans". Root spans are spans that have no local parent span. Root spans may however have incoming trace data from a different service, for example when using distributed tracing. + +- The `childProcessIntegration`'s (previously `processThreadBreadcrumbIntegration`) `name` value has been changed from `"ProcessAndThreadBreadcrumbs"` to `"ChildProcess"`. This is relevant if you were filtering integrations by name. + +- The Prisma integration no longer supports Prisma v5 and supports Prisma v6 by default. As per Prisma v6, the `previewFeatures = ["tracing"]` client generator option in your Prisma Schema is no longer required to use tracing with the Prisma integration. + + For performance instrumentation using other/older Prisma versions: + + 1. Install the `@prisma/instrumentation` package with the desired version. + 1. Pass a `new PrismaInstrumentation()` instance as exported from `@prisma/instrumentation` to the `prismaInstrumentation` option of this integration: + + ```js + import { PrismaInstrumentation } from '@prisma/instrumentation'; + Sentry.init({ + integrations: [ + prismaIntegration({ + // Override the default instrumentation that Sentry uses + prismaInstrumentation: new PrismaInstrumentation(), + }), + ], + }); + ``` + + The passed instrumentation instance will override the default instrumentation instance the integration would use, while the `prismaIntegration` will still ensure data compatibility for the various Prisma versions. + + 1. Depending on your Prisma version (prior to Prisma version 6), add `previewFeatures = ["tracing"]` to the client generator block of your Prisma schema: + + ``` + generator client { + provider = "prisma-client-js" + previewFeatures = ["tracing"] + } + ``` + +### `@sentry/browser` + +- The SDK no longer instructs the Sentry backend to automatically infer IP addresses by default. This means that places where you previously saw IP addresses in Sentry may now be grouped to anonymous users. Set the `sendDefaultPii` option in `Sentry.init()` to true to instruct the Sentry backend to infer IP addresses. +- The `captureUserFeedback` method has been removed. Use the `captureFeedback` method instead and update the `comments` field to `message`. + +### `@sentry/nextjs` + +- The Sentry Next.js SDK will no longer use the Next.js Build ID as fallback identifier for releases. The SDK will continue to attempt to read CI-provider-specific environment variables and the current git SHA to automatically determine a release name. If you examine that you no longer see releases created in Sentry, it is recommended to manually provide a release name to `withSentryConfig` via the `release.name` option. + + This behavior was changed because the Next.js Build ID is non-deterministic and the release name is injected into client bundles, causing build artifacts to be non-deterministic. This caused issues for some users. Additionally, because it is uncertain whether it will be possible to rely on a Build ID when Turbopack becomes stable, we decided to pull the plug now instead of introducing confusing behavior in the future. + +- Source maps are now automatically enabled for both client and server builds unless explicitly disabled via `sourcemaps.disable`. Client builds use `hidden-source-map` while server builds use `source-map` as their webpack `devtool` setting unless any other value than `false` or `undefined` has been assigned already. + +- By default, source maps will now be automatically deleted after being uploaded to Sentry for client-side builds. You can opt out of this behavior by explicitly setting `sourcemaps.deleteSourcemapsAfterUpload` to `false` in your Sentry config. + +- The `sentry` property on the Next.js config object has officially been discontinued. Pass options to `withSentryConfig` directly. + +### All Meta-Framework SDKs (`@sentry/astro`, `@sentry/nuxt`, `@sentry/solidstart`) + +- Updated source map generation to respect the user-provided value of your build config, such as `vite.build.sourcemap`: + + - Explicitly disabled (false): Emit warning, no source map upload. + - Explicitly enabled (true, 'hidden', 'inline'): No changes, source maps are uploaded and not automatically deleted. + - Unset: Enable 'hidden', delete `.map` files after uploading them to Sentry. + + To customize which files are deleted after upload, define the `filesToDeleteAfterUpload` array with globs. + +### `@sentry/react` + +The `componentStack` field in the `ErrorBoundary` component is now typed as `string` instead of `string | null | undefined` for the `onError` and `onReset` lifecycle methods. This more closely matches the actual behavior of React, which always returns a `string` whenever a component stack is available. + +In the `onUnmount` lifecycle method, the `componentStack` field is now typed as `string | null`. The `componentStack` is `null` when no error has been thrown at time of unmount. + +### Uncategorized (TODO) + +TODO + +## 3. Package Removals + +As part of an architectural cleanup, we deprecated the following packages: + +- `@sentry/utils` +- `@sentry/types` + +All of these packages exports and APIs have been moved into the `@sentry/core` package. + +The `@sentry/utils` package will no longer be published. + +The `@sentry/types` package will continue to be published but it is deprecated and we don't plan on extending its API. +You may experience slight compatibility issues in the future by using it. +We decided to keep this package around to temporarily lessen the upgrade burden. +It will be removed in a future major version. + +## 4. Removal of Deprecated APIs (TODO) + +### `@sentry/core` / All SDKs + +- The `debugIntegration` has been removed. To log outgoing events, use [Hook Options](https://docs.sentry.io/platforms/javascript/configuration/options/#hooks) (`beforeSend`, `beforeSendTransaction`, ...). +- The `sessionTimingIntegration` has been removed. To capture session durations alongside events, use [Context](https://docs.sentry.io/platforms/javascript/enriching-events/context/) (`Sentry.setContext()`). +- The `addOpenTelemetryInstrumentation` method has been removed. Use the `openTelemetryInstrumentations` option in `Sentry.init()` or your custom Sentry Client instead. + +```js +import * as Sentry from '@sentry/node'; + +// before +Sentry.addOpenTelemetryInstrumentation(new GenericPoolInstrumentation()); + +// after +Sentry.init({ + openTelemetryInstrumentations: [new GenericPoolInstrumentation()], +}); +``` + +- The `DEFAULT_USER_INCLUDES` constant has been removed. +- The `getCurrentHub()`, `Hub` and `getCurrentHubShim()` APIs have been removed. They were on compatibility life support since the release of v8 and have now been fully removed from the SDK. + +### `@sentry/browser` + +- The `captureUserFeedback` method has been removed. Use the `captureFeedback` method instead and update the `comments` field to `message`. + +### `@sentry/core` + +- The `getNumberOfUrlSegments` method has been removed. There is no replacement. +- The `validSeverityLevels` export has been removed. There is no replacement. +- The `makeFifoCache` method has been removed. There is no replacement. +- The `arrayify` export has been removed. There is no replacement. +- The `BAGGAGE_HEADER_NAME` export has been removed. Use the `"baggage"` string constant directly instead. +- The `flatten` export has been removed. There is no replacement. +- The `urlEncode` method has been removed. There is no replacement. +- The `getDomElement` method has been removed. There is no replacement. +- The `memoBuilder` method has been removed. There is no replacement. +- The `extractRequestData` method has been removed. Manually extract relevant data off request instead. +- The `addRequestDataToEvent` method has been removed. Use `httpRequestToRequestData` instead and put the resulting object directly on `event.request`. +- The `extractPathForTransaction` method has been removed. There is no replacement. +- The `addNormalizedRequestDataToEvent` method has been removed. Use `httpRequestToRequestData` instead and put the resulting object directly on `event.request`. +- A `sampleRand` field on `PropagationContext` is now required. This is relevant if you used `scope.setPropagationContext(...)` + +#### Other/Internal Changes + +The following changes are unlikely to affect users of the SDK. They are listed here only for completion sake, and to alert users that may be relying on internal behavior. + +- `client._prepareEvent()` now requires a currentScope & isolationScope to be passed as last arugments +- `client.recordDroppedEvent()` no longer accepts an `event` as third argument. The event was no longer used for some time, instead you can (optionally) pass a count of dropped events as third argument. + +### `@sentry/nestjs` + +- Removed `WithSentry` decorator. Use the `SentryExceptionCaptured` decorator instead. +- Removed `SentryService`. + - If you are using `@sentry/nestjs` you can safely remove any references to the `SentryService`. + - If you are using another package migrate to `@sentry/nestjs` and remove the `SentryService` afterward. +- Removed `SentryTracingInterceptor`. + - If you are using `@sentry/nestjs` you can safely remove any references to the `SentryTracingInterceptor`. + - If you are using another package migrate to `@sentry/nestjs` and remove the `SentryTracingInterceptor` afterward. +- Removed `SentryGlobalGenericFilter`. + - Use the `SentryGlobalFilter` instead. + - The `SentryGlobalFilter` is a drop-in replacement. +- Removed `SentryGlobalGraphQLFilter`. + - Use the `SentryGlobalFilter` instead. + - The `SentryGlobalFilter` is a drop-in replacement. + +### `@sentry/react` + +- The `wrapUseRoutes` method has been removed. Use the `wrapUseRoutesV6` or `wrapUseRoutesV7` methods instead depending on what version of react router you are using. +- The `wrapCreateBrowserRouter` method has been removed. Use the `wrapCreateBrowserRouterV6` or `wrapCreateBrowserRouterV7` methods depending on what version of react router you are using. + +## `@sentry/vue` + +- The options `tracingOptions`, `trackComponents`, `timeout`, `hooks` have been removed everywhere except in the `tracingOptions` option of `vueIntegration()`. + + These options should now be set as follows: + + ```js + import * as Sentry from '@sentry/vue'; + + Sentry.init({ + integrations: [ + Sentry.vueIntegration({ + tracingOptions: { + trackComponents: true, + timeout: 1000, + hooks: ['mount', 'update', 'unmount'], + }, + }), + ], + }); + ``` + +- The option `logErrors` in the `vueIntegration` has been removed. The Sentry Vue error handler will propagate the error to a user-defined error handler + or just re-throw the error (which will log the error without modifying). + +### `@sentry/opentelemetry` + +- Removed `getPropagationContextFromSpan`. + This function was primarily internally used. + It's functionality was misleading and should not be used. + +### `@sentry/sveltekit` + +- The `fetchProxyScriptNonce` option in `sentryHandle()` was removed due to security concerns. If you previously specified this option for your CSP policy, specify a [script hash](https://docs.sentry.io/platforms/javascript/guides/sveltekit/manual-setup/#configure-csp-for-client-side-fetch-instrumentation) in your CSP config or [disable](https://docs.sentry.io/platforms/javascript/guides/sveltekit/manual-setup/#disable-client-side-fetch-proxy-script) the injection of the script entirely. + +## 5. Build Changes + +Previously the CJS versions of the SDK code (wrongfully) contained compatibility statements for default exports in ESM: + +```js +Object.defineProperty(exports, '__esModule', { value: true }); +``` + +The SDK no longer contains these statements. +Let us know if this is causing issues in your setup by opening an issue on GitHub. + +### `@sentry/deno` + +The minimum supported Deno version is now **2.0.0**. + +- `@sentry/deno` is no longer published on `deno.land` so you'll need to import + from npm: + +```javascript +import * as Sentry from 'npm:@sentry/deno'; + +Sentry.init({ + dsn: '__DSN__', + // ... +}); +``` + +## 6. Type Changes + +In v8, types have been exported from `@sentry/types`, while implementations have been exported from other classes. + +This led to some duplication, where we had to keep an interface in `@sentry/types`, while the implementation mirroring that interface was kept e.g. in `@sentry/core`. + +Since v9, the types have been merged into `@sentry/core`, which removed some of this duplication. This means that certain things that used to be a separate interface, will not expect an actual instance of the class/concrete implementation. + +This should not affect most users unless you relied on passing things with a similar shape to internal methods. The following types are affected: + +- `Scope` now always expects the `Scope` class +- The `TransactionNamingScheme` type has been removed. There is no replacement. +- The `Request` type has been removed. Use `RequestEventData` type instead. +- The `IntegrationClass` type is no longer exported - it was not used anymore. Instead, use `Integration` or `IntegrationFn`. +- The `samplingContext.request` attribute in the `tracesSampler` has been removed. Use `samplingContext.normalizedRequest` instead. Note that the type of `normalizedRequest` differs from `request`. +- The `samplingContext.transactionContext` object in the `tracesSampler` has been removed. All object attributes are available in the top-level of `samplingContext`. +- `Client` now always expects the `BaseClient` class - there is no more abstract `Client` that can be implemented! Any `Client` class has to extend from `BaseClient`. +- `ReportDialogOptions` now extends `Record` instead of `Record` - this should not affect most users. +- The `RequestDataIntegrationOptions` type has been removed. There is no replacement. + +# No Version Support Timeline + +Version support timelines are stressful for anybody using the SDK, so we won't be defining one. +Instead, we will be applying bug fixes and features to older versions as long as there is demand for them. + +We also hold ourselves to high standards security-wise, meaning that if any vulnerabilities are found, we will in almost all cases backport them. + +Note, that we will decide on a case-per-case basis, what gets backported or not. +If you need a fix or feature in a previous version of the SDK, feel free to reach out via a GitHub issue. + +# Deprecations in 8.x + +The following outlines deprecations that were introduced in version 8 of the SDK. + +## General + +- **Returning `null` from `beforeSendSpan` span is deprecated.** + + Returning `null` from `beforeSendSpan` will now result in a warning being logged. + In v9, dropping spans is not possible anymore within this hook. + +- **Passing `undefined` to `tracesSampleRate` / `tracesSampler` / `enableTracing` will be handled differently in v9** + +In v8, explicitly setting `tracesSampleRate` (even if it is set to `undefined`) resulted in tracing being _enabled_, although no spans were generated. + +```ts +Sentry.init({ + tracesSampleRate: undefined, +}); +``` + +In v9, we will streamline this behavior so that passing `undefined` will result in tracing being disabled, the same as not passing the option at all. + +If you are relying on `undefined` being passed in and having tracing enabled because of this, you should update your config to set e.g. `tracesSampleRate: 0` instead, which will also enable tracing in v9. + +The `enableTracing` option was removed. In v9, to emulate `enableTracing: true`, set `tracesSampleRate: 1`. To emulate `enableTracing: false`, remove the `tracesSampleRate` and `tracesSampler` options (if configured). + +- **The `autoSessionTracking` option is deprecated.** + +To enable session tracking, it is recommended to unset `autoSessionTracking` and ensure that either, in browser environments the `browserSessionIntegration` is added, or in server environments the `httpIntegration` is added. + +To disable session tracking, it is recommended unset `autoSessionTracking` and to remove the `browserSessionIntegration` in browser environments, or in server environments configure the `httpIntegration` with the `trackIncomingRequestsAsSessions` option set to `false`. +Additionally, in Node.js environments, a session was automatically created for every node process when `autoSessionTracking` was set to `true`. This behavior has been replaced by the `processSessionIntegration` which is configured by default. + +- **The metrics API has been removed from the SDK.** + +The Sentry metrics beta has ended and the metrics API has been removed from the SDK. Learn more in [help center docs](https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Metrics-Beta-Ended-on-October-7th). + +## `@sentry/utils` + +- **The `@sentry/utils` package has been deprecated. Import everything from `@sentry/core` instead.** + +- Deprecated `AddRequestDataToEventOptions.transaction`. This option effectively doesn't do anything anymore, and will be removed in v9. +- Deprecated `TransactionNamingScheme` type. +- Deprecated `validSeverityLevels`. Will not be replaced. +- Deprecated `urlEncode`. No replacements. +- Deprecated `addRequestDataToEvent`. Use `httpRequestToRequestData` instead and put the resulting object directly on `event.request`. +- Deprecated `extractRequestData`. Instead manually extract relevant data off request. +- Deprecated `arrayify`. No replacements. +- Deprecated `memoBuilder`. No replacements. +- Deprecated `getNumberOfUrlSegments`. No replacements. +- Deprecated `BAGGAGE_HEADER_NAME`. Use the `"baggage"` string constant directly instead. +- Deprecated `makeFifoCache`. No replacements. +- Deprecated `dynamicRequire`. No replacements. +- Deprecated `flatten`. No replacements. +- Deprecated `_browserPerformanceTimeOriginMode`. No replacements. + +## `@sentry/core` + +- Deprecated `transactionNamingScheme` option in `requestDataIntegration`. +- Deprecated `debugIntegration`. To log outgoing events, use [Hook Options](https://docs.sentry.io/platforms/javascript/configuration/options/#hooks) (`beforeSend`, `beforeSendTransaction`, ...). +- Deprecated `sessionTimingIntegration`. To capture session durations alongside events, use [Context](https://docs.sentry.io/platforms/javascript/enriching-events/context/) (`Sentry.setContext()`). +- Deprecated `addTracingHeadersToFetchRequest` method - this was only meant for internal use and is not needed anymore. +- Deprecated `generatePropagationContext()` in favor of using `generateTraceId()` directly. +- Deprecated `spanId` field on `propagationContext` - this field will be removed in v9, and should neither be read or set anymore. +- Deprecated `RequestSession` type. No replacements. +- Deprecated `RequestSessionStatus` type. No replacements. +- Deprecated `SessionFlusherLike` type. No replacements. +- Deprecated `SessionFlusher`. No replacements. +- Deprecated `initSessionFlusher` on `ServerRuntimeClient`. No replacements. The `httpIntegration` will flush sessions by itself. + +## `@sentry/nestjs` + +- Deprecated the `@WithSentry` decorator. Use the `@SentryExceptionCaptured` decorator instead. +- Deprecated the `SentryTracingInterceptor` method. + If you are using `@sentry/nestjs` you can safely remove any references to the `SentryTracingInterceptor`. + If you are using another package migrate to `@sentry/nestjs` and remove the `SentryTracingInterceptor` afterward. +- Deprecated `SentryService`. + If you are using `@sentry/nestjs` you can safely remove any references to the `SentryService`. + If you are using another package migrate to `@sentry/nestjs` and remove the `SentryService` afterward. +- Deprecated `SentryGlobalGenericFilter`. + Use the `SentryGlobalFilter` instead. + The `SentryGlobalFilter` is a drop-in replacement. +- Deprecated `SentryGlobalGraphQLFilter`. + Use the `SentryGlobalFilter` instead. + The `SentryGlobalFilter` is a drop-in replacement. + +## `@sentry/types` + +- **The `@sentry/types` package has been deprecated. Import everything from `@sentry/core` instead.** + +- Deprecated `Request` in favor of `RequestEventData`. +- Deprecated `RequestSession`. No replacements. +- Deprecated `RequestSessionStatus`. No replacements. +- Deprecated `SessionFlusherLike`. No replacements. + +## `@sentry/nuxt` + +- Deprecated `tracingOptions` in `Sentry.init()` in favor of passing the `vueIntegration()` to `Sentry.init({ integrations: [...] })` and setting `tracingOptions` there. + +## `@sentry/vue` + +- Deprecated `tracingOptions`, `trackComponents`, `timeout`, `hooks` options everywhere other than in the `tracingOptions` option of the `vueIntegration()`. + +These options should now be set as follows: + +```ts +import * as Sentry from '@sentry/vue'; + +Sentry.init({ + integrations: [ + Sentry.vueIntegration({ + tracingOptions: { + trackComponents: true, + timeout: 1000, + hooks: ['mount', 'update', 'unmount'], + }, + }), + ], +}); +``` + +- Deprecated `logErrors` in the `vueIntegration`. The Sentry Vue error handler will propagate the error to a user-defined error handler + or just re-throw the error (which will log the error without modifying). + +## `@sentry/nuxt` and `@sentry/vue` + +- When component tracking is enabled, "update" spans are no longer created by default. + +Add an `"update"` item to the `tracingOptions.hooks` option via the `vueIntegration()` to restore this behavior. + +```ts +Sentry.init({ + integrations: [ + Sentry.vueIntegration({ + tracingOptions: { + trackComponents: true, + hooks: [ + 'mount', + 'update', // <-- + 'unmount', + ], + }, + }), + ], +}); +``` + +## `@sentry/astro` + +- Deprecated passing `dsn`, `release`, `environment`, `sampleRate`, `tracesSampleRate`, `replaysSessionSampleRate` to the integration. Use the runtime-specific `Sentry.init()` calls for passing these options instead. + +## `@sentry/remix` + +- Deprecated `autoInstrumentRemix: false`. The next major version will default to behaving as if this option were `true` and the option itself will be removed. + +## `@sentry/react` + +- Deprecated `wrapUseRoutes`. Use the `wrapUseRoutesV6` or `wrapUseRoutesV7` methods instead. +- Deprecated `wrapCreateBrowserRouter`. Use the `wrapCreateBrowserRouterV6` or `wrapCreateBrowserRouterV7` methods instead. + +## `@sentry/nextjs` + +- Deprecated the `hideSourceMaps` option. There are no replacements for this option. The SDK emits hidden sourcemaps by default. + +### `@sentry/sveltekit` + +- The `fetchProxyScriptNonce` option in `sentryHandle()` was deprecated due to security concerns. If you previously specified this option for your CSP policy, specify a [script hash](https://docs.sentry.io/platforms/javascript/guides/sveltekit/manual-setup/#configure-csp-for-client-side-fetch-instrumentation) in your CSP config or [disable](https://docs.sentry.io/platforms/javascript/guides/sveltekit/manual-setup/#disable-client-side-fetch-proxy-script) the injection of the script entirely. + +## `@sentry/opentelemetry` + +- Deprecated the `generateSpanContextForPropagationContext` method. There are no replacements for this method. + +## Server-side SDKs (`@sentry/node` and all dependents) + +- Deprecated `processThreadBreadcrumbIntegration` in favor of `childProcessIntegration`. Functionally they are the same. +- Deprecated `nestIntegration`. Use the NestJS SDK (`@sentry/nestjs`) instead. +- Deprecated `setupNestErrorHandler`. Use the NestJS SDK (`@sentry/nestjs`) instead. +- Deprecated `addOpenTelemetryInstrumentation`. Use the `openTelemetryInstrumentations` option in `Sentry.init()` or your custom Sentry Client instead. +- Deprecated `registerEsmLoaderHooks.include` and `registerEsmLoaderHooks.exclude`. Set `onlyIncludeInstrumentedModules: true` instead. +- `registerEsmLoaderHooks` will only accept `true | false | undefined` in the future. The SDK will default to wrapping modules that are used as part of OpenTelemetry Instrumentation. +- `httpIntegration({ spans: false })` is configured by default if `skipOpenTelemetrySetup: true` is set. You can still overwrite this if desired. diff --git a/docs/publishing-a-release.md b/docs/publishing-a-release.md index 7b88d6bd41b8..290c6a3076c8 100644 --- a/docs/publishing-a-release.md +++ b/docs/publishing-a-release.md @@ -20,6 +20,23 @@ _These steps are only relevant to Sentry employees when preparing and publishing [@getsentry/releases-approvers](https://github.com/orgs/getsentry/teams/release-approvers) to approve the release. a. Once the release is completed, a sync from `master` ->` develop` will be automatically triggered +## Publishing a release for previous majors + +1. Run `yarn changelog` on a previous major branch (e.g. `v8`) and determine what version will be released (we use + [semver](https://semver.org)) +2. Create a branch, e.g. `changelog-8.45.1`, off a previous major branch (e.g. `v8`) +3. Update `CHANGELOG.md` to add an entry for the next release number and a list of changes since the + last release. (See details below.) +4. Open a PR with the title `meta(changelog): Update changelog for VERSION` against the previous major branch (e.g. `v8`). +5. **Be cautious!** The PR against the previous major branch should be merged via "Squash and Merge" + (as the commits already exist on this branch). +6. Once the PR is merged, open the [Prepare Release workflow](https://github.com/getsentry/sentry-javascript/actions/workflows/release.yml) and + fill in ![run-release-workflow.png](./assets/run-release-workflow.png) + 1. The major branch you want to release for, e.g. `v8` + 2. The version you want to release, e.g. `8.45.1` + 3. The major branch to merge into, e.g. `v8` +7. Run the release workflow + ## Updating the Changelog 1. Run `yarn changelog` and copy everything. diff --git a/package.json b/package.json index e948ae773c72..73ae7f18495d 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "circularDepCheck": "lerna run circularDepCheck", "clean": "run-s clean:build clean:caches", "clean:build": "lerna run clean", - "clean:caches": "yarn rimraf eslintcache .nxcache && yarn jest --clearCache", + "clean:caches": "yarn rimraf eslintcache .nxcache .nx && yarn jest --clearCache", "clean:deps": "lerna clean --yes && rm -rf node_modules && yarn", "clean:tarballs": "rimraf {packages,dev-packages}/*/*.tgz", "clean:watchman": "watchman watch-del \".\"", @@ -31,6 +31,7 @@ "lint:lerna": "lerna run lint", "lint:biome": "biome check .", "lint:prettier": "prettier \"**/*.md\" \"**/*.css\" --check", + "lint:es-compatibility": "es-check es2020 ./packages/*/build/{bundles,npm/cjs,cjs}/*.js && es-check es2020 ./packages/*/build/{npm/esm,esm}/*.js --module", "postpublish": "lerna run --stream --concurrency 1 postpublish", "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\" test", "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\" test:unit", @@ -44,9 +45,9 @@ "yalc:publish": "lerna run yalc:publish" }, "volta": { - "node": "18.20.3", + "node": "18.20.5", "yarn": "1.22.22", - "pnpm": "9.4.0" + "pnpm": "9.15.0" }, "workspaces": [ "packages/angular", @@ -67,7 +68,6 @@ "packages/integration-shims", "packages/nestjs", "packages/nextjs", - "packages/nitro-utils", "packages/node", "packages/nuxt", "packages/opentelemetry", @@ -83,7 +83,6 @@ "packages/sveltekit", "packages/types", "packages/typescript", - "packages/utils", "packages/vercel-edge", "packages/vue", "packages/wasm", @@ -112,10 +111,11 @@ "@size-limit/webpack": "~11.1.6", "@types/jest": "^27.4.1", "@types/jsdom": "^21.1.6", - "@types/node": "^14.18.0", - "@vitest/coverage-v8": "^1.6.0", + "@types/node": "^18.19.1", + "@vitest/coverage-v8": "^2.1.8", "deepmerge": "^4.2.2", "downlevel-dts": "~0.11.0", + "es-check": "^7.2.1", "eslint": "7.32.0", "jest": "^27.5.1", "jest-environment-node": "^27.5.1", @@ -133,8 +133,8 @@ "sucrase": "^3.35.0", "ts-jest": "^27.1.4", "ts-node": "10.9.1", - "typescript": "4.9.5", - "vitest": "^1.6.0", + "typescript": "~5.0.0", + "vitest": "^2.1.8", "yalc": "^1.0.0-pre.53" }, "//_resolutions_comment": [ @@ -145,7 +145,8 @@ "resolutions": { "gauge/strip-ansi": "6.0.1", "wide-align/string-width": "4.2.3", - "cliui/wrap-ansi": "7.0.0" + "cliui/wrap-ansi": "7.0.0", + "**/sucrase": "getsentry/sucrase#es2020-polyfills" }, "version": "0.0.0", "name": "sentry-javascript", diff --git a/packages/angular/.eslintrc.cjs b/packages/angular/.eslintrc.cjs index 5a263ad7adbb..f7b591f35685 100644 --- a/packages/angular/.eslintrc.cjs +++ b/packages/angular/.eslintrc.cjs @@ -4,4 +4,8 @@ module.exports = { }, extends: ['../../.eslintrc.js'], ignorePatterns: ['setup-test.ts', 'patch-vitest.ts'], + rules: { + // Angular transpiles this correctly/relies on this + '@sentry-internal/sdk/no-class-field-initializers': 'off', + }, }; diff --git a/packages/angular/README.md b/packages/angular/README.md index 42ee54a8d81c..95e0379480d7 100644 --- a/packages/angular/README.md +++ b/packages/angular/README.md @@ -17,7 +17,7 @@ ## Angular Version Compatibility -This SDK officially supports Angular 15 to 17. +This SDK officially supports Angular 14 to 19. If you're using an older Angular version please check the [compatibility table in the docs](https://docs.sentry.io/platforms/javascript/guides/angular/#angular-version-compatibility). @@ -33,24 +33,17 @@ in `@sentry/browser` can be imported from `@sentry/angular`. To use this SDK, call `Sentry.init(options)` before you bootstrap your Angular application. ```javascript -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { bootstrapApplication } from '@angular/platform-browser'; import { init } from '@sentry/angular'; -import { AppModule } from './app/app.module'; +import { AppComponent } from './app/app.component'; init({ dsn: '__DSN__', // ... }); -// ... - -enableProdMode(); -platformBrowserDynamic() - .bootstrapModule(AppModule) - .then(success => console.log(`Bootstrap success`)) - .catch(err => console.error(err)); +bootstrapApplication(AppComponent, appConfig); ``` ### ErrorHandler @@ -58,10 +51,22 @@ platformBrowserDynamic() `@sentry/angular` 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'; +```ts +import { ApplicationConfig, NgModule, ErrorHandler } from '@angular/core'; import { createErrorHandler } from '@sentry/angular'; +export const appConfig: ApplicationConfig = { + providers: [ + { + provide: ErrorHandler, + useValue: createErrorHandler({ + showDialog: true, + }), + }, + ], +}; + +// Or using an old module approach: @NgModule({ // ... providers: [ @@ -104,42 +109,27 @@ init({ }); ``` -2. Register `SentryTrace` as a provider in Angular's DI system, with a `Router` as its dependency: +2. Inject the `TraceService` in the `APP_INITIALIZER`: -```javascript -import { NgModule } from '@angular/core'; -import { Router } from '@angular/router'; -import { TraceService } from '@sentry/angular'; +```ts +import { ApplicationConfig, APP_INITIALIZER, provideAppInitializer } from '@angular/core'; -@NgModule({ - // ... +export const appConfig: ApplicationConfig = { providers: [ { - provide: TraceService, - deps: [Router], + provide: APP_INITIALIZER, + useFactory: () => () => {}, + deps: [TraceService], + multi: true, }, - ], - // ... -}) -export class AppModule {} -``` - -3. Either require the `TraceService` from inside `AppModule` or use `APP_INITIALIZER` to force-instantiate Tracing. - -```javascript -@NgModule({ - // ... -}) -export class AppModule { - constructor(trace: TraceService) {} -} -``` - -or -```javascript -import { APP_INITIALIZER } from '@angular/core'; + // Starting with Angular 19, we can use `provideAppInitializer` + // instead of directly providing `APP_INITIALIZER` (deprecated): + provideAppInitializer(() => inject(TraceService)), + ], +}; +// Or using an old module approach: @NgModule({ // ... providers: [ @@ -149,6 +139,10 @@ import { APP_INITIALIZER } from '@angular/core'; deps: [TraceService], multi: true, }, + + // Starting with Angular 19, we can use `provideAppInitializer` + // instead of directly providing `APP_INITIALIZER` (deprecated): + provideAppInitializer(() => inject(TraceService)), ], // ... }) @@ -161,15 +155,15 @@ To track Angular components as part of your transactions, you have 3 options. _TraceDirective:_ used to track a duration between `OnInit` and `AfterViewInit` lifecycle hooks in template: -```javascript +```ts import { TraceModule } from '@sentry/angular'; -@NgModule({ - // ... +@Component({ + selector: 'some-component', imports: [TraceModule], // ... }) -export class AppModule {} +export class SomeComponentThatUsesTraceDirective {} ``` Then, inside your component's template (keep in mind that the directive's name attribute is required): diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json index 0bbbbdcdef14..4a2d2eac3db3 100644 --- a/packages/angular/ng-package.json +++ b/packages/angular/ng-package.json @@ -4,6 +4,6 @@ "lib": { "entryFile": "src/index.ts" }, - "allowedNonPeerDependencies": ["@sentry/browser", "@sentry/core", "@sentry/utils", "@sentry/types", "tslib"], + "allowedNonPeerDependencies": ["@sentry/browser", "@sentry/core", "@sentry/types", "tslib"], "assets": [] } diff --git a/packages/angular/package.json b/packages/angular/package.json index 06bb0492c2f7..1ec948299d01 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.18" + "node": ">=18" }, "type": "module", "module": "build/fesm2015/sentry-angular.mjs", @@ -35,6 +35,7 @@ "@angular/platform-browser": "^14.3.0", "@angular/platform-browser-dynamic": "^14.3.0", "@angular/router": "^14.3.0", + "@types/node": "^14.8.0", "ng-packagr": "^14.2.2", "rxjs": "7.8.1", "typescript": "4.6.4", diff --git a/packages/angular/patch-vitest.ts b/packages/angular/patch-vitest.ts index 9789b0da0a92..476d40860786 100644 --- a/packages/angular/patch-vitest.ts +++ b/packages/angular/patch-vitest.ts @@ -182,22 +182,20 @@ function isAngularFixture(val: any): boolean { */ function fixtureVitestSerializer(fixture: any) { // * Get Component meta data - const componentType = ( - fixture && fixture.componentType ? fixture.componentType : fixture.componentRef.componentType - ) as any; + const componentType = (fixture?.componentType ? fixture.componentType : fixture.componentRef.componentType) as any; let inputsData: string = ''; const selector = Reflect.getOwnPropertyDescriptor(componentType, '__annotations__')?.value[0].selector; - if (componentType && componentType.propDecorators) { + if (componentType?.propDecorators) { inputsData = Object.entries(componentType.propDecorators) .map(([key, value]) => `${key}="${value}"`) .join(''); } // * Get DOM Elements - const divElement = fixture && fixture.nativeElement ? fixture.nativeElement : fixture.location.nativeElement; + const divElement = fixture?.nativeElement ? fixture.nativeElement : fixture.location.nativeElement; // * Convert string data to HTML data const doc = new DOMParser().parseFromString( diff --git a/packages/angular/src/sdk.ts b/packages/angular/src/sdk.ts index 5bfb6882851b..404305f770ff 100755 --- a/packages/angular/src/sdk.ts +++ b/packages/angular/src/sdk.ts @@ -23,7 +23,7 @@ import { IS_DEBUG_BUILD } from './flags'; /** * Get the default integrations for the Angular SDK. */ -export function getDefaultIntegrations(options: BrowserOptions = {}): Integration[] { +export function getDefaultIntegrations(_options: BrowserOptions = {}): Integration[] { // Don't include the BrowserApiErrors integration as it interferes with the Angular SDK's `ErrorHandler`: // BrowserApiErrors would catch certain errors before they reach the `ErrorHandler` and // thus provide a lower fidelity error than what `SentryErrorHandler` @@ -32,7 +32,7 @@ export function getDefaultIntegrations(options: BrowserOptions = {}): Integratio // see: // - https://github.com/getsentry/sentry-javascript/issues/5417#issuecomment-1453407097 // - https://github.com/getsentry/sentry-javascript/issues/2744 - const integrations = [ + return [ inboundFiltersIntegration(), functionToStringIntegration(), breadcrumbsIntegration(), @@ -40,14 +40,8 @@ export function getDefaultIntegrations(options: BrowserOptions = {}): Integratio linkedErrorsIntegration(), dedupeIntegration(), httpContextIntegration(), + browserSessionIntegration(), ]; - - // eslint-disable-next-line deprecation/deprecation - if (options.autoSessionTracking !== false) { - integrations.push(browserSessionIntegration()); - } - - return integrations; } /** @@ -68,7 +62,7 @@ export function init(options: BrowserOptions): Client | undefined { function checkAndSetAngularVersion(): void { const ANGULAR_MINIMUM_VERSION = 14; - const angularVersion = VERSION && VERSION.major ? parseInt(VERSION.major, 10) : undefined; + const angularVersion = VERSION?.major && parseInt(VERSION.major, 10); if (angularVersion) { if (angularVersion < ANGULAR_MINIMUM_VERSION) { diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index c347a5e19b2e..6ac94b362d13 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +import { ElementRef } from '@angular/core'; import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core'; import { Directive, Injectable, Input, NgModule } from '@angular/core'; import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router'; @@ -235,10 +237,17 @@ export class TraceService implements OnDestroy { } } -const UNKNOWN_COMPONENT = 'unknown'; - /** - * A directive that can be used to capture initialization lifecycle of the whole component. + * Captures the initialization lifecycle of the component this directive is applied to. + * Specifically, this directive measures the time between `ngOnInit` and `ngAfterViewInit` + * of the component. + * + * Falls back to the component's selector if no name is provided. + * + * @example + * ```html + * + * ``` */ @Directive({ selector: '[trace]' }) export class TraceDirective implements OnInit, AfterViewInit { @@ -246,13 +255,19 @@ export class TraceDirective implements OnInit, AfterViewInit { private _tracingSpan?: Span; + public constructor(private readonly _host: ElementRef) {} + /** * Implementation of OnInit lifecycle method * @inheritdoc */ public ngOnInit(): void { if (!this.componentName) { - this.componentName = UNKNOWN_COMPONENT; + // Technically, the `trace` binding should always be provided. + // However, if it is incorrectly declared on the element without a + // value (e.g., ``), we fall back to using `tagName` + // (which is e.g. `APP-COMPONENT`). + this.componentName = this._host.nativeElement.tagName.toLowerCase(); } if (getActiveSpan()) { @@ -307,7 +322,7 @@ export function TraceClass(options?: TraceClassOptions): ClassDecorator { tracingSpan = runOutsideAngular(() => startInactiveSpan({ onlyIfParent: true, - name: `<${options && options.name ? options.name : 'unnamed'}>`, + name: `<${options?.name || 'unnamed'}>`, op: ANGULAR_INIT_OP, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', @@ -352,7 +367,7 @@ export function TraceMethod(options?: TraceMethodOptions): MethodDecorator { runOutsideAngular(() => { startInactiveSpan({ onlyIfParent: true, - name: `<${options && options.name ? options.name : 'unnamed'}>`, + name: `<${options?.name ? options.name : 'unnamed'}>`, op: `${ANGULAR_OP}.${String(propertyKey)}`, startTime: now, attributes: { @@ -382,9 +397,9 @@ export function TraceMethod(options?: TraceMethodOptions): MethodDecorator { export function getParameterizedRouteFromSnapshot(route?: ActivatedRouteSnapshot | null): string { const parts: string[] = []; - let currentRoute = route && route.firstChild; + let currentRoute = route?.firstChild; while (currentRoute) { - const path = currentRoute && currentRoute.routeConfig && currentRoute.routeConfig.path; + const path = currentRoute?.routeConfig && currentRoute.routeConfig.path; if (path === null || path === undefined) { break; } diff --git a/packages/angular/src/zone.ts b/packages/angular/src/zone.ts index fdd45bdf8b0c..22f56e4c3871 100644 --- a/packages/angular/src/zone.ts +++ b/packages/angular/src/zone.ts @@ -8,7 +8,7 @@ declare const Zone: any; // Therefore, it's advisable to safely check whether the `run` function is // available in the `` context. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -const isNgZoneEnabled = typeof Zone !== 'undefined' && Zone.root && Zone.root.run; +const isNgZoneEnabled = typeof Zone !== 'undefined' && Zone.root?.run; /** * The function that does the same job as `NgZone.runOutsideAngular`. diff --git a/packages/angular/vitest.config.ts b/packages/angular/vitest.config.ts index 9f09af3b153e..82015893133b 100644 --- a/packages/angular/vitest.config.ts +++ b/packages/angular/vitest.config.ts @@ -1,10 +1,9 @@ -import type { UserConfig } from 'vitest'; import { defineConfig } from 'vitest/config'; import baseConfig from '../../vite/vite.config'; export default defineConfig({ test: { - ...(baseConfig as UserConfig & { test: any }).test, + ...baseConfig.test, coverage: {}, globals: true, setupFiles: ['./setup-test.ts'], diff --git a/packages/astro/.eslintrc.cjs b/packages/astro/.eslintrc.cjs index c706032aaf35..29b78099e7c6 100644 --- a/packages/astro/.eslintrc.cjs +++ b/packages/astro/.eslintrc.cjs @@ -11,12 +11,5 @@ module.exports = { project: ['tsconfig.test.json'], }, }, - { - files: ['src/integration/**', 'src/server/**'], - rules: { - '@sentry-internal/sdk/no-optional-chaining': 'off', - '@sentry-internal/sdk/no-nullish-coalescing': 'off', - }, - }, ], }; diff --git a/packages/astro/package.json b/packages/astro/package.json index 43c374a766cc..1103c6df1093 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -14,7 +14,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=18.19.1" }, "type": "module", "files": [ @@ -63,7 +63,7 @@ }, "devDependencies": { "astro": "^3.5.0", - "vite": "^5.4.10" + "vite": "^5.4.11" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 7eca9de9a41a..57abd7efede3 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -11,10 +11,6 @@ export { addBreadcrumb, addEventProcessor, addIntegration, - // eslint-disable-next-line deprecation/deprecation - addOpenTelemetryInstrumentation, - // eslint-disable-next-line deprecation/deprecation - addRequestDataToEvent, amqplibIntegration, anrIntegration, disableAnrDetectionForCallback, @@ -34,16 +30,11 @@ export { createTransport, cron, dataloaderIntegration, - // eslint-disable-next-line deprecation/deprecation - debugIntegration, dedupeIntegration, - DEFAULT_USER_INCLUDES, defaultStackParser, endSession, expressErrorHandler, expressIntegration, - // eslint-disable-next-line deprecation/deprecation - extractRequestData, extraErrorDataIntegration, fastifyIntegration, flush, @@ -54,8 +45,6 @@ export { getActiveSpan, getAutoPerformanceIntegrations, getClient, - // eslint-disable-next-line deprecation/deprecation - getCurrentHub, getCurrentScope, getDefaultIntegrations, getGlobalScope, @@ -80,16 +69,12 @@ export { localVariablesIntegration, lruMemoizerIntegration, makeNodeTransport, - // eslint-disable-next-line deprecation/deprecation - metrics, modulesIntegration, mongoIntegration, mongooseIntegration, mysql2Integration, mysqlIntegration, nativeNodeFetchIntegration, - // eslint-disable-next-line deprecation/deprecation - nestIntegration, NodeClient, nodeContextIntegration, onUncaughtExceptionIntegration, @@ -97,8 +82,6 @@ export { parameterize, postgresIntegration, prismaIntegration, - // eslint-disable-next-line deprecation/deprecation - processThreadBreadcrumbIntegration, childProcessIntegration, redisIntegration, requestDataIntegration, @@ -109,8 +92,6 @@ export { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - // eslint-disable-next-line deprecation/deprecation - sessionTimingIntegration, setContext, setCurrentClient, setExtra, @@ -123,8 +104,6 @@ export { setupExpressErrorHandler, setupHapiErrorHandler, setupKoaErrorHandler, - // eslint-disable-next-line deprecation/deprecation - setupNestErrorHandler, setUser, spanToBaggageHeader, spanToJSON, @@ -138,6 +117,7 @@ export { startSpanManual, tediousIntegration, trpcMiddleware, + updateSpanName, withActiveSpan, withIsolationScope, withMonitor, diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index ce87a51c3af7..eeadf11fa3d5 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -10,7 +10,6 @@ import type { NodeOptions } from '@sentry/node'; import type { Client, Integration, Options, StackParser } from '@sentry/core'; import type * as clientSdk from './index.client'; -import type * as serverSdk from './index.server'; import sentryAstro from './index.server'; /** Initializes Sentry Astro SDK */ @@ -25,13 +24,6 @@ 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 getCurrentHub: typeof clientSdk.getCurrentHub; -export declare const getClient: typeof clientSdk.getClient; -export declare const continueTrace: typeof clientSdk.continueTrace; - export declare const Span: clientSdk.Span; -// eslint-disable-next-line deprecation/deprecation -export declare const metrics: typeof clientSdk.metrics & typeof serverSdk; export default sentryAstro; diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 49e33ff0231d..79b74b8804c1 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { sentryVitePlugin } from '@sentry/vite-plugin'; import type { AstroConfig, AstroIntegration } from 'astro'; -import { dropUndefinedKeys } from '@sentry/core'; +import { consoleSandbox, dropUndefinedKeys } from '@sentry/core'; import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from './snippets'; import type { SentryOptions } from './types'; @@ -35,19 +35,31 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { // We don't need to check for AUTH_TOKEN here, because the plugin will pick it up from the env if (shouldUploadSourcemaps && command !== 'dev') { - // TODO(v9): Remove this warning - if (config?.vite?.build?.sourcemap === false) { - logger.warn( - "You disabled sourcemaps with the `vite.build.sourcemap` option. Currently, the Sentry SDK will override this option to generate sourcemaps. In future versions, the Sentry SDK will not override the `vite.build.sourcemap` option if you explicitly disable it. If you want to generate and upload sourcemaps please set the `vite.build.sourcemap` option to 'hidden' or undefined.", - ); - } + const computedSourceMapSettings = getUpdatedSourceMapSettings(config, options); + + let updatedFilesToDeleteAfterUpload: string[] | undefined = undefined; + + if ( + typeof uploadOptions?.filesToDeleteAfterUpload === 'undefined' && + computedSourceMapSettings.previousUserSourceMapSetting === 'unset' + ) { + // This also works for adapters, as the source maps are also copied to e.g. the .vercel folder + updatedFilesToDeleteAfterUpload = ['./dist/**/client/**/*.map', './dist/**/server/**/*.map']; - // TODO: Add deleteSourcemapsAfterUpload option and warn if it isn't set. + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + `[Sentry] Automatically setting \`sourceMapsUploadOptions.filesToDeleteAfterUpload: ${JSON.stringify( + updatedFilesToDeleteAfterUpload, + )}\` to delete generated source maps after they were uploaded to Sentry.`, + ); + }); + } updateConfig({ vite: { build: { - sourcemap: true, + sourcemap: computedSourceMapSettings.updatedSourceMapSetting, }, plugins: [ sentryVitePlugin( @@ -58,6 +70,8 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { telemetry: uploadOptions.telemetry ?? true, sourcemaps: { assets: uploadOptions.assets ?? [getSourcemapsAssetsGlob(config)], + filesToDeleteAfterUpload: + uploadOptions?.filesToDeleteAfterUpload ?? updatedFilesToDeleteAfterUpload, }, bundleSizeOptimizations: { ...options.bundleSizeOptimizations, @@ -171,3 +185,73 @@ function getSourcemapsAssetsGlob(config: AstroConfig): string { // fallback to default output dir return 'dist/**/*'; } + +/** + * Whether the user enabled (true, 'hidden', 'inline') or disabled (false) source maps + */ +export type UserSourceMapSetting = 'enabled' | 'disabled' | 'unset' | undefined; + +/** There are 3 ways to set up source map generation (https://github.com/getsentry/sentry-javascript/issues/13993) + * + * 1. User explicitly disabled source maps + * - keep this setting (emit a warning that errors won't be unminified in Sentry) + * - We won't upload anything + * + * 2. Users enabled source map generation (true, 'hidden', 'inline'). + * - keep this setting (don't do anything - like deletion - besides uploading) + * + * 3. Users didn't set source maps generation + * - we enable 'hidden' source maps generation + * - configure `filesToDeleteAfterUpload` to delete all .map files (we emit a log about this) + * + * --> only exported for testing + */ +export function getUpdatedSourceMapSettings( + astroConfig: AstroConfig, + sentryOptions?: SentryOptions, +): { previousUserSourceMapSetting: UserSourceMapSetting; updatedSourceMapSetting: boolean | 'inline' | 'hidden' } { + let previousUserSourceMapSetting: UserSourceMapSetting = undefined; + + astroConfig.build = astroConfig.build || {}; + + const viteSourceMap = astroConfig?.vite?.build?.sourcemap; + let updatedSourceMapSetting = viteSourceMap; + + const settingKey = 'vite.build.sourcemap'; + + if (viteSourceMap === false) { + previousUserSourceMapSetting = 'disabled'; + updatedSourceMapSetting = viteSourceMap; + + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] Source map generation is currently disabled in your Astro configuration (\`${settingKey}: false\`). This setting is either a default setting or was explicitly set in your configuration. Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified. To show unminified code, enable source maps in \`${settingKey}\` (e.g. by setting them to \`hidden\`).`, + ); + }); + } else if (viteSourceMap && ['hidden', 'inline', true].includes(viteSourceMap)) { + previousUserSourceMapSetting = 'enabled'; + updatedSourceMapSetting = viteSourceMap; + + if (sentryOptions?.debug) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + `[Sentry] We discovered \`${settingKey}\` is set to \`${viteSourceMap.toString()}\`. Sentry will keep this source map setting. This will un-minify the code snippet on the Sentry Issue page.`, + ); + }); + } + } else { + previousUserSourceMapSetting = 'unset'; + updatedSourceMapSetting = 'hidden'; + + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + `[Sentry] Enabled source map generation in the build options with \`${settingKey}: 'hidden'\`. The source maps will be deleted after they were uploaded to Sentry.`, + ); + }); + } + + return { previousUserSourceMapSetting, updatedSourceMapSetting }; +} diff --git a/packages/astro/src/integration/types.ts b/packages/astro/src/integration/types.ts index b32b62556140..6c2e41808eca 100644 --- a/packages/astro/src/integration/types.ts +++ b/packages/astro/src/integration/types.ts @@ -73,6 +73,16 @@ type SourceMapsOptions = { * @see https://www.npmjs.com/package/glob#glob-primer */ assets?: string | Array; + + /** + * A glob or an array of globs that specifies the build artifacts that should be deleted after the artifact + * upload to Sentry has been completed. + * + * @default [] - By default no files are deleted. + * + * The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob) + */ + filesToDeleteAfterUpload?: string | Array; }; type BundleSizeOptimizationOptions = { diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index de381de9d5ed..6b55dbd8a976 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -87,11 +87,13 @@ async function instrumentRequest( isolationScope?: Scope, ): Promise { // Make sure we don't accidentally double wrap (e.g. user added middleware and integration auto added it) - const locals = ctx.locals as AstroLocalsWithSentry; - if (locals && locals.__sentry_wrapped__) { + const locals = ctx.locals as AstroLocalsWithSentry | undefined; + if (locals?.__sentry_wrapped__) { return next(); } - addNonEnumerableProperty(locals, '__sentry_wrapped__', true); + if (locals) { + addNonEnumerableProperty(locals, '__sentry_wrapped__', true); + } const isDynamicPageRequest = checkIsDynamicPageRequest(ctx); @@ -164,7 +166,7 @@ async function instrumentRequest( const client = getClient(); const contentType = originalResponse.headers.get('content-type'); - const isPageloadRequest = contentType && contentType.startsWith('text/html'); + const isPageloadRequest = contentType?.startsWith('text/html'); if (!isPageloadRequest || !client) { return originalResponse; } diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index 41ff4bbae061..9f3ea651697d 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -52,7 +52,6 @@ describe('Sentry client SDK', () => { it.each([ ['tracesSampleRate', { tracesSampleRate: 0 }], ['tracesSampler', { tracesSampler: () => 1.0 }], - ['enableTracing', { enableTracing: true }], ['no tracing option set', {}], ])('adds browserTracingIntegration if tracing is enabled via %s', (_, tracingOptions) => { init({ @@ -72,7 +71,7 @@ describe('Sentry client SDK', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - enableTracing: true, + tracesSampleRate: 1, }); const integrationsToInit = browserInit.mock.calls[0]![0]?.defaultIntegrations || []; @@ -90,7 +89,7 @@ describe('Sentry client SDK', () => { integrations: [ browserTracingIntegration({ finalTimeout: 10, instrumentNavigation: false, instrumentPageLoad: false }), ], - enableTracing: true, + tracesSampleRate: 1, }); const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); diff --git a/packages/astro/test/integration/index.test.ts b/packages/astro/test/integration/index.test.ts index eb6bdf555ae3..6684a841ba4e 100644 --- a/packages/astro/test/integration/index.test.ts +++ b/packages/astro/test/integration/index.test.ts @@ -1,4 +1,7 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { AstroConfig } from 'astro'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getUpdatedSourceMapSettings } from '../../src/integration/index'; +import type { SentryOptions } from '../../src/integration/types'; import { sentryAstro } from '../../src/integration'; @@ -31,7 +34,7 @@ describe('sentryAstro integration', () => { expect(integration.name).toBe('@sentry/astro'); }); - it('enables source maps and adds the sentry vite plugin if an auth token is detected', async () => { + it('enables "hidden" source maps, adds filesToDeleteAfterUpload and adds the sentry vite plugin if an auth token is detected', async () => { const integration = sentryAstro({ sourceMapsUploadOptions: { enabled: true, org: 'my-org', project: 'my-project', telemetry: false }, }); @@ -44,7 +47,7 @@ describe('sentryAstro integration', () => { expect(updateConfig).toHaveBeenCalledWith({ vite: { build: { - sourcemap: true, + sourcemap: 'hidden', }, plugins: ['sentryVitePlugin'], }, @@ -60,6 +63,7 @@ describe('sentryAstro integration', () => { bundleSizeOptimizations: {}, sourcemaps: { assets: ['out/**/*'], + filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'], }, _metaOptions: { telemetry: { @@ -86,6 +90,7 @@ describe('sentryAstro integration', () => { bundleSizeOptimizations: {}, sourcemaps: { assets: ['dist/**/*'], + filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'], }, _metaOptions: { telemetry: { @@ -119,6 +124,7 @@ describe('sentryAstro integration', () => { bundleSizeOptimizations: {}, sourcemaps: { assets: ['{.vercel,dist}/**/*'], + filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'], }, _metaOptions: { telemetry: { @@ -157,6 +163,7 @@ describe('sentryAstro integration', () => { bundleSizeOptimizations: {}, sourcemaps: { assets: ['dist/server/**/*, dist/client/**/*'], + filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'], }, _metaOptions: { telemetry: { @@ -166,6 +173,35 @@ describe('sentryAstro integration', () => { }); }); + it('prefers user-specified filesToDeleteAfterUpload over the default values', async () => { + const integration = sentryAstro({ + sourceMapsUploadOptions: { + enabled: true, + org: 'my-org', + project: 'my-project', + filesToDeleteAfterUpload: ['./custom/path/**/*'], + }, + }); + // @ts-expect-error - the hook exists, and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ + updateConfig, + injectScript, + // @ts-expect-error - only passing in partial config + config: { + outDir: new URL('file://path/to/project/build'), + }, + }); + + expect(sentryVitePluginSpy).toHaveBeenCalledTimes(1); + expect(sentryVitePluginSpy).toHaveBeenCalledWith( + expect.objectContaining({ + sourcemaps: expect.objectContaining({ + filesToDeleteAfterUpload: ['./custom/path/**/*'], + }), + }), + ); + }); + it("doesn't enable source maps if `sourceMapsUploadOptions.enabled` is `false`", async () => { const integration = sentryAstro({ sourceMapsUploadOptions: { enabled: false }, @@ -373,3 +409,60 @@ describe('sentryAstro integration', () => { expect(addMiddleware).toHaveBeenCalledTimes(0); }); }); + +describe('getUpdatedSourceMapSettings', () => { + let astroConfig: Omit & { vite: { build: { sourcemap?: any } } }; + let sentryOptions: SentryOptions; + + beforeEach(() => { + astroConfig = { vite: { build: {} } } as Omit & { vite: { build: { sourcemap?: any } } }; + sentryOptions = {}; + }); + + it('should keep explicitly disabled source maps disabled', () => { + astroConfig.vite.build.sourcemap = false; + const result = getUpdatedSourceMapSettings(astroConfig, sentryOptions); + expect(result.previousUserSourceMapSetting).toBe('disabled'); + expect(result.updatedSourceMapSetting).toBe(false); + }); + + it('should keep explicitly enabled source maps enabled', () => { + const cases = [ + { sourcemap: true, expected: true }, + { sourcemap: 'hidden', expected: 'hidden' }, + { sourcemap: 'inline', expected: 'inline' }, + ]; + + cases.forEach(({ sourcemap, expected }) => { + astroConfig.vite.build.sourcemap = sourcemap; + const result = getUpdatedSourceMapSettings(astroConfig, sentryOptions); + expect(result.previousUserSourceMapSetting).toBe('enabled'); + expect(result.updatedSourceMapSetting).toBe(expected); + }); + }); + + it('should enable "hidden" source maps when unset', () => { + astroConfig.vite.build.sourcemap = undefined; + const result = getUpdatedSourceMapSettings(astroConfig, sentryOptions); + expect(result.previousUserSourceMapSetting).toBe('unset'); + expect(result.updatedSourceMapSetting).toBe('hidden'); + }); + + it('should log warnings and messages when debug is enabled', () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + sentryOptions = { debug: true }; + + astroConfig.vite.build.sourcemap = false; + getUpdatedSourceMapSettings(astroConfig, sentryOptions); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Source map generation is currently disabled')); + + astroConfig.vite.build.sourcemap = 'hidden'; + getUpdatedSourceMapSettings(astroConfig, sentryOptions); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Sentry will keep this source map setting')); + + consoleWarnSpy.mockRestore(); + consoleLogSpy.mockRestore(); + }); +}); diff --git a/packages/aws-serverless/.eslintrc.js b/packages/aws-serverless/.eslintrc.js index 99fcba0976da..d1d4c4e12aa0 100644 --- a/packages/aws-serverless/.eslintrc.js +++ b/packages/aws-serverless/.eslintrc.js @@ -3,9 +3,6 @@ module.exports = { node: true, }, extends: ['../../.eslintrc.js'], - rules: { - '@sentry-internal/sdk/no-optional-chaining': 'off', - }, overrides: [ { files: ['scripts/**/*.ts'], diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 856a7dc4f51f..ed3900d0d86f 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.18" + "node": ">=18" }, "files": [ "/build/npm", @@ -54,7 +54,7 @@ } }, "typesVersions": { - "<4.9": { + "<5.0": { "build/npm/types/index.d.ts": [ "build/npm/types-ts3.8/index.d.ts" ] @@ -65,15 +65,15 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/instrumentation": "^0.56.0", - "@opentelemetry/instrumentation-aws-lambda": "0.49.0", - "@opentelemetry/instrumentation-aws-sdk": "0.48.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/instrumentation-aws-lambda": "0.50.2", + "@opentelemetry/instrumentation-aws-sdk": "0.49.0", "@sentry/core": "8.45.0", "@sentry/node": "8.45.0", "@types/aws-lambda": "^8.10.62" }, "devDependencies": { - "@types/node": "^14.18.0" + "@types/node": "^18.19.1" }, "scripts": { "build": "run-p build:transpile build:types build:bundle", diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 3f167b62a7e3..60747de09dd5 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -12,8 +12,6 @@ export { endSession, withMonitor, createTransport, - // eslint-disable-next-line deprecation/deprecation - getCurrentHub, getClient, isInitialized, generateInstrumentOnce, @@ -42,11 +40,6 @@ export { flush, close, getSentryRelease, - // eslint-disable-next-line deprecation/deprecation - addRequestDataToEvent, - DEFAULT_USER_INCLUDES, - // eslint-disable-next-line deprecation/deprecation - extractRequestData, createGetModuleFromFilename, anrIntegration, disableAnrDetectionForCallback, @@ -76,8 +69,6 @@ export { continueTrace, getAutoPerformanceIntegrations, cron, - // eslint-disable-next-line deprecation/deprecation - metrics, parameterize, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -104,14 +95,8 @@ export { mysql2Integration, redisIntegration, tediousIntegration, - // eslint-disable-next-line deprecation/deprecation - nestIntegration, - // eslint-disable-next-line deprecation/deprecation - setupNestErrorHandler, postgresIntegration, prismaIntegration, - // eslint-disable-next-line deprecation/deprecation - processThreadBreadcrumbIntegration, childProcessIntegration, hapiIntegration, setupHapiErrorHandler, @@ -121,8 +106,7 @@ export { spanToTraceHeader, spanToBaggageHeader, trpcMiddleware, - // eslint-disable-next-line deprecation/deprecation - addOpenTelemetryInstrumentation, + updateSpanName, zodErrorsIntegration, profiler, amqplibIntegration, @@ -130,13 +114,9 @@ export { export { captureConsoleIntegration, - // eslint-disable-next-line deprecation/deprecation - debugIntegration, dedupeIntegration, extraErrorDataIntegration, rewriteFramesIntegration, - // eslint-disable-next-line deprecation/deprecation - sessionTimingIntegration, } from '@sentry/core'; export { awsIntegration } from './integration/aws'; diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts index fc67aaa432ef..34921d24c7ff 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -51,7 +51,7 @@ export interface WrapperOptions { captureAllSettledReasons: boolean; /** * Automatically trace all handler invocations. - * You may want to disable this if you use express within Lambda (use tracingHandler instead). + * You may want to disable this if you use express within Lambda. * @default true */ startTrace: boolean; @@ -220,10 +220,7 @@ function enhanceScopeWithEnvironmentData(scope: Scope, context: Context, startTi * @param context AWS Lambda context that will be used to extract some part of the data */ function enhanceScopeWithTransactionData(scope: Scope, context: Context): void { - scope.addEventProcessor(event => { - event.transaction = context.functionName; - return event; - }); + scope.setTransactionName(context.functionName); scope.setTag('server_name', process.env._AWS_XRAY_DAEMON_ADDRESS || process.env.SENTRY_NAME || hostname()); scope.setTag('url', `awslambda:///${context.functionName}`); } @@ -323,7 +320,7 @@ export function wrapHandler( throw e; } finally { clearTimeout(timeoutWarningTimer); - if (span && span.isRecording()) { + if (span?.isRecording()) { span.end(); } await flush(options.flushTimeout).catch(e => { @@ -336,7 +333,7 @@ export function wrapHandler( // Only start a trace and root span if the handler is not already wrapped by Otel instrumentation // Otherwise, we create two root spans (one from otel, one from our wrapper). // If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler. - // TODO(v9): Since bumping the OTEL Instrumentation, this is likely not needed anymore, we can possibly remove this + // TODO: Since bumping the OTEL Instrumentation, this is likely not needed anymore, we can possibly remove this (can be done whenever since it would be non-breaking) if (options.startTrace && !isWrappedByOtel(handler)) { const traceData = getAwsTraceData(event as { headers?: Record }, context); diff --git a/packages/aws-serverless/src/utils.ts b/packages/aws-serverless/src/utils.ts index e330fb01dc13..73038003e534 100644 --- a/packages/aws-serverless/src/utils.ts +++ b/packages/aws-serverless/src/utils.ts @@ -53,7 +53,7 @@ export function getAwsTraceData(event: HandlerEvent, context?: HandlerContext): baggage: headers.baggage, }; - if (context && context.clientContext && context.clientContext.Custom) { + if (context?.clientContext?.Custom) { const customContext: Record = context.clientContext.Custom; const sentryTrace = isString(customContext['sentry-trace']) ? customContext['sentry-trace'] : undefined; diff --git a/packages/aws-serverless/test/sdk.test.ts b/packages/aws-serverless/test/sdk.test.ts index 7ab59670cdf2..28b58a830e61 100644 --- a/packages/aws-serverless/test/sdk.test.ts +++ b/packages/aws-serverless/test/sdk.test.ts @@ -18,6 +18,7 @@ const mockScope = { setTag: jest.fn(), setContext: jest.fn(), addEventProcessor: jest.fn(), + setTransactionName: jest.fn(), }; jest.mock('@sentry/node', () => { @@ -81,12 +82,8 @@ const fakeCallback: Callback = (err, result) => { }; function expectScopeSettings() { - expect(mockScope.addEventProcessor).toBeCalledTimes(1); - // Test than an event processor to add `transaction` is registered for the scope - const eventProcessor = mockScope.addEventProcessor.mock.calls[0][0]; - const event: Event = {}; - eventProcessor(event); - expect(event).toEqual({ transaction: 'functionName' }); + expect(mockScope.setTransactionName).toBeCalledTimes(1); + expect(mockScope.setTransactionName).toBeCalledWith('functionName'); expect(mockScope.setTag).toBeCalledWith('server_name', expect.anything()); diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 15d5bde00065..b18c3e3dfa51 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.18" + "node": ">=18" }, "files": [ "/build" @@ -29,7 +29,7 @@ } }, "typesVersions": { - "<4.9": { + "<5.0": { "build/types/index.d.ts": [ "build/types-ts3.8/index.d.ts" ] diff --git a/packages/browser-utils/src/getNativeImplementation.ts b/packages/browser-utils/src/getNativeImplementation.ts index 42d402eb1b94..398a40045119 100644 --- a/packages/browser-utils/src/getNativeImplementation.ts +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -47,7 +47,7 @@ export function getNativeImplementation { const globalObject = WINDOW as unknown as Record; - const targetObj = globalObject[target]; - const proto = targetObj && targetObj.prototype; + const proto = globalObject[target]?.prototype; // eslint-disable-next-line no-prototype-builtins - if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) { + if (!proto?.hasOwnProperty?.('addEventListener')) { return; } @@ -170,7 +169,7 @@ function shouldSkipDOMEvent(eventType: string, target: SentryWrappedTarget | nul return false; } - if (!target || !target.tagName) { + if (!target?.tagName) { return true; } diff --git a/packages/browser-utils/src/instrument/history.ts b/packages/browser-utils/src/instrument/history.ts index 0a166c6fe111..ad5ecb75f2ed 100644 --- a/packages/browser-utils/src/instrument/history.ts +++ b/packages/browser-utils/src/instrument/history.ts @@ -1,5 +1,5 @@ -import { addHandler, fill, maybeInstrument, supportsHistory, triggerHandlers } from '@sentry/core'; import type { HandlerDataHistory } from '@sentry/core'; +import { addHandler, fill, maybeInstrument, supportsHistory, triggerHandlers } from '@sentry/core'; import { WINDOW } from '../types'; let lastHref: string | undefined; @@ -19,29 +19,26 @@ export function addHistoryInstrumentationHandler(handler: (data: HandlerDataHist } function instrumentHistory(): void { - if (!supportsHistory()) { - return; - } - - const oldOnPopState = WINDOW.onpopstate; - WINDOW.onpopstate = function (this: WindowEventHandlers, ...args: unknown[]) { + // The `popstate` event may also be triggered on `pushState`, but it may not always reliably be emitted by the browser + // Which is why we also monkey-patch methods below, in addition to this + WINDOW.addEventListener('popstate', () => { const to = WINDOW.location.href; // keep track of the current URL state, as we always receive only the updated state const from = lastHref; lastHref = to; - const handlerData: HandlerDataHistory = { from, to }; - triggerHandlers('history', handlerData); - if (oldOnPopState) { - // Apparently this can throw in Firefox when incorrectly implemented plugin is installed. - // https://github.com/getsentry/sentry-javascript/issues/3344 - // https://github.com/bugsnag/bugsnag-js/issues/469 - try { - return oldOnPopState.apply(this, args); - } catch (_oO) { - // no-empty - } + + if (from === to) { + return; } - }; + + const handlerData = { from, to } satisfies HandlerDataHistory; + triggerHandlers('history', handlerData); + }); + + // Just guard against this not being available, in weird environments + if (!supportsHistory()) { + return; + } function historyReplacementFunction(originalHistoryFunction: () => void): () => void { return function (this: History, ...args: unknown[]): void { @@ -52,7 +49,12 @@ function instrumentHistory(): void { const to = String(url); // keep track of the current URL state, as we always receive only the updated state lastHref = to; - const handlerData: HandlerDataHistory = { from, to }; + + if (from === to) { + return; + } + + const handlerData = { from, to } satisfies HandlerDataHistory; triggerHandlers('history', handlerData); } return originalHistoryFunction.apply(this, args); diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index aadde247642c..1d2b6b47c87e 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -83,7 +83,7 @@ interface StartTrackingWebVitalsOptions { */ export function startTrackingWebVitals({ recordClsStandaloneSpans }: StartTrackingWebVitalsOptions): () => void { const performance = getBrowserPerformanceAPI(); - if (performance && browserPerformanceTimeOrigin) { + if (performance && browserPerformanceTimeOrigin()) { // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are if (performance.mark) { WINDOW.performance.mark('sentry-tracing-init'); @@ -97,7 +97,7 @@ export function startTrackingWebVitals({ recordClsStandaloneSpans }: StartTracki fidCleanupCallback(); lcpCleanupCallback(); ttfbCleanupCallback(); - clsCleanupCallback && clsCleanupCallback(); + clsCleanupCallback?.(); }; } @@ -117,7 +117,7 @@ export function startTrackingLongTasks(): void { const { op: parentOp, start_timestamp: parentStartTimestamp } = spanToJSON(parent); for (const entry of entries) { - const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); + const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime); const duration = msToSec(entry.duration); if (parentOp === 'navigation' && parentStartTimestamp && startTime < parentStartTimestamp) { @@ -156,7 +156,7 @@ export function startTrackingLongAnimationFrames(): void { continue; } - const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); + const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime); const { start_timestamp: parentStartTimestamp, op: parentOp } = spanToJSON(parent); @@ -167,7 +167,6 @@ export function startTrackingLongAnimationFrames(): void { // routing instrumentations continue; } - const duration = msToSec(entry.duration); const attributes: SpanAttributes = { @@ -210,7 +209,7 @@ export function startTrackingInteractions(): void { } for (const entry of entries) { if (entry.name === 'click') { - const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); + const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime); const duration = msToSec(entry.duration); const spanOptions: StartSpanOptions & Required> = { @@ -271,7 +270,7 @@ function _trackFID(): () => void { return; } - const timeOrigin = msToSec(browserPerformanceTimeOrigin as number); + const timeOrigin = msToSec(browserPerformanceTimeOrigin() as number); const startTime = msToSec(entry.startTime); _measurements['fid'] = { value: metric.value, unit: 'millisecond' }; _measurements['mark.fid'] = { value: timeOrigin + startTime, unit: 'second' }; @@ -300,12 +299,13 @@ interface AddPerformanceEntriesOptions { /** Add performance related spans to a transaction */ export function addPerformanceEntries(span: Span, options: AddPerformanceEntriesOptions): void { const performance = getBrowserPerformanceAPI(); - if (!performance || !performance.getEntries || !browserPerformanceTimeOrigin) { + const origin = browserPerformanceTimeOrigin(); + if (!performance?.getEntries || !origin) { // Gatekeeper if performance API not available return; } - const timeOrigin = msToSec(browserPerformanceTimeOrigin); + const timeOrigin = msToSec(origin); const performanceEntries = performance.getEntries(); @@ -674,7 +674,7 @@ function _setWebVitalAttributes(span: Span): void { } // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift - if (_clsEntry && _clsEntry.sources) { + if (_clsEntry?.sources) { _clsEntry.sources.forEach((source, index) => span.setAttribute(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), ); diff --git a/packages/browser-utils/src/metrics/cls.ts b/packages/browser-utils/src/metrics/cls.ts index 43ff84c01965..f9a6c662d79d 100644 --- a/packages/browser-utils/src/metrics/cls.ts +++ b/packages/browser-utils/src/metrics/cls.ts @@ -73,14 +73,16 @@ export function trackClsAsStandaloneSpan(): void { const unsubscribeStartNavigation = client.on('startNavigationSpan', () => { _collectClsOnce(); - unsubscribeStartNavigation && unsubscribeStartNavigation(); + unsubscribeStartNavigation?.(); }); const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - const spanJSON = rootSpan && spanToJSON(rootSpan); - if (spanJSON && spanJSON.op === 'pageload') { - pageloadSpanId = rootSpan.spanContext().spanId; + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + const spanJSON = spanToJSON(rootSpan); + if (spanJSON.op === 'pageload') { + pageloadSpanId = rootSpan.spanContext().spanId; + } } }, 0); } @@ -88,15 +90,15 @@ export function trackClsAsStandaloneSpan(): void { function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined, pageloadSpanId: string) { DEBUG_BUILD && logger.log(`Sending CLS span (${clsValue})`); - const startTime = msToSec((browserPerformanceTimeOrigin || 0) + ((entry && entry.startTime) || 0)); + const startTime = msToSec((browserPerformanceTimeOrigin() || 0) + (entry?.startTime || 0)); const routeName = getCurrentScope().getScopeData().transactionName; - const name = entry ? htmlTreeAsString(entry.sources[0] && entry.sources[0].node) : 'Layout shift'; + const name = entry ? htmlTreeAsString(entry.sources[0]?.node) : 'Layout shift'; const attributes: SpanAttributes = dropUndefinedKeys({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.cls', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.webvital.cls', - [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: (entry && entry.duration) || 0, + [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry?.duration || 0, // attach the pageload span id to the CLS span so that we can link them in the UI 'sentry.pageload.span_id': pageloadSpanId, }); diff --git a/packages/browser-utils/src/metrics/inp.ts b/packages/browser-utils/src/metrics/inp.ts index 7ef99b4d32fd..64ea9cccaca0 100644 --- a/packages/browser-utils/src/metrics/inp.ts +++ b/packages/browser-utils/src/metrics/inp.ts @@ -28,7 +28,7 @@ const INTERACTIONS_SPAN_MAP = new Map(); */ export function startTrackingINP(): () => void { const performance = getBrowserPerformanceAPI(); - if (performance && browserPerformanceTimeOrigin) { + if (performance && browserPerformanceTimeOrigin()) { const inpCallback = _trackINP(); return (): void => { @@ -85,7 +85,7 @@ function _trackINP(): () => void { const interactionType = INP_ENTRY_MAP[entry.name]; /** Build the INP span, create an envelope from the span, and then send the envelope */ - const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); + const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime); const duration = msToSec(metric.value); const activeSpan = getActiveSpan(); const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; @@ -127,9 +127,8 @@ function _trackINP(): () => void { /** * Register a listener to cache route information for INP interactions. - * TODO(v9): `latestRoute` no longer needs to be passed in and will be removed in v9. */ -export function registerInpInteractionListener(_latestRoute?: unknown): void { +export function registerInpInteractionListener(): void { const handleEntries = ({ entries }: { entries: PerformanceEntry[] }): void => { const activeSpan = getActiveSpan(); const activeRootSpan = activeSpan && getRootSpan(activeSpan); diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts index 3f78c2e28605..b273f9d3eb5e 100644 --- a/packages/browser-utils/src/metrics/instrument.ts +++ b/packages/browser-utils/src/metrics/instrument.ts @@ -201,7 +201,7 @@ export function addPerformanceInstrumentationHandler( function triggerHandlers(type: InstrumentHandlerType, data: unknown): void { const typeHandlers = handlers[type]; - if (!typeHandlers || !typeHandlers.length) { + if (!typeHandlers?.length) { return; } diff --git a/packages/browser-utils/src/metrics/utils.ts b/packages/browser-utils/src/metrics/utils.ts index b6bc9fc54f2f..91aefa8a8918 100644 --- a/packages/browser-utils/src/metrics/utils.ts +++ b/packages/browser-utils/src/metrics/utils.ts @@ -74,11 +74,11 @@ export function startStandaloneWebVitalSpan(options: StandaloneWebVitalSpanOptio const { name, transaction, attributes: passedAttributes, startTime } = options; - const { release, environment } = client.getOptions(); + const { release, environment, sendDefaultPii } = client.getOptions(); // We need to get the replay, user, and activeTransaction from the current scope // so that we can associate replay id, profile id, and a user display to the span const replay = client.getIntegrationByName string }>('Replay'); - const replayId = replay && replay.getReplayId(); + const replayId = replay?.getReplayId(); const scope = getCurrentScope(); @@ -106,7 +106,10 @@ export function startStandaloneWebVitalSpan(options: StandaloneWebVitalSpanOptio // Web vital score calculation relies on the user agent to account for different // browsers setting different thresholds for what is considered a good/meh/bad value. // For example: Chrome vs. Chrome Mobile - 'user_agent.original': WINDOW.navigator && WINDOW.navigator.userAgent, + 'user_agent.original': WINDOW.navigator?.userAgent, + + // This tells Sentry to infer the IP address from the request + 'client.address': sendDefaultPii ? '{{auto}}' : undefined, ...passedAttributes, }; @@ -124,7 +127,7 @@ export function startStandaloneWebVitalSpan(options: StandaloneWebVitalSpanOptio /** Get the browser performance API. */ export function getBrowserPerformanceAPI(): Performance | undefined { // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are - return WINDOW && WINDOW.addEventListener && WINDOW.performance; + return WINDOW.addEventListener && WINDOW.performance; } /** diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getActivationStart.ts b/packages/browser-utils/src/metrics/web-vitals/lib/getActivationStart.ts index 84c742098aab..4bdafc0c718c 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getActivationStart.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/getActivationStart.ts @@ -18,5 +18,5 @@ import { getNavigationEntry } from './getNavigationEntry'; export const getActivationStart = (): number => { const navEntry = getNavigationEntry(); - return (navEntry && navEntry.activationStart) || 0; + return navEntry?.activationStart || 0; }; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts b/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts index 1e8521c2ddc6..f2c85f6127bc 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts @@ -19,8 +19,7 @@ import { WINDOW } from '../../../types'; // sentry-specific change: // add optional param to not check for responseStart (see comment below) export const getNavigationEntry = (checkResponseStart = true): PerformanceNavigationTiming | void => { - const navigationEntry = - WINDOW.performance && WINDOW.performance.getEntriesByType && WINDOW.performance.getEntriesByType('navigation')[0]; + const navigationEntry = WINDOW.performance?.getEntriesByType?.('navigation')[0]; // Check to ensure the `responseStart` property is present and valid. // In some cases no value is reported by the browser (for // privacy/security reasons), and in other cases (bugs) the value is diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts b/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts index fee96d83bf33..b2cfbc609a25 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts @@ -25,9 +25,9 @@ export const initMetric = (name: MetricNa let navigationType: MetricType['navigationType'] = 'navigate'; if (navEntry) { - if ((WINDOW.document && WINDOW.document.prerendering) || getActivationStart() > 0) { + if (WINDOW.document?.prerendering || getActivationStart() > 0) { navigationType = 'prerender'; - } else if (WINDOW.document && WINDOW.document.wasDiscarded) { + } else if (WINDOW.document?.wasDiscarded) { navigationType = 'restore'; } else if (navEntry.type) { navigationType = navEntry.type.replace(/_/g, '-') as MetricType['navigationType']; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/interactions.ts b/packages/browser-utils/src/metrics/web-vitals/lib/interactions.ts index 6d6390755656..69ca920ddb67 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/interactions.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/interactions.ts @@ -113,7 +113,7 @@ export const processInteractionEntry = (entry: PerformanceEventTiming) => { existingInteraction.latency = entry.duration; } else if ( entry.duration === existingInteraction.latency && - entry.startTime === (existingInteraction.entries[0] && existingInteraction.entries[0].startTime) + entry.startTime === existingInteraction.entries[0]?.startTime ) { existingInteraction.entries.push(entry); } diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts b/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts index 81d83caa53b5..f1640d4fcdac 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts @@ -32,7 +32,7 @@ export interface OnHiddenCallback { // simulate the page being hidden. export const onHidden = (cb: OnHiddenCallback) => { const onHiddenOrPageHide = (event: Event) => { - if (event.type === 'pagehide' || (WINDOW.document && WINDOW.document.visibilityState === 'hidden')) { + if (event.type === 'pagehide' || WINDOW.document?.visibilityState === 'hidden') { cb(event); } }; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts b/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts index 8463a1d199ef..e5e1ecd45385 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts @@ -17,7 +17,7 @@ import { WINDOW } from '../../../types'; export const whenActivated = (callback: () => void) => { - if (WINDOW.document && WINDOW.document.prerendering) { + if (WINDOW.document?.prerendering) { addEventListener('prerenderingchange', () => callback(), true); } else { callback(); diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdle.ts b/packages/browser-utils/src/metrics/web-vitals/lib/whenIdle.ts index c140864b3539..8914c45d7bb3 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdle.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/whenIdle.ts @@ -30,7 +30,7 @@ export const whenIdle = (cb: () => void): number => { cb = runOnce(cb) as () => void; // If the document is hidden, run the callback immediately, otherwise // race an idle callback with the next `visibilitychange` event. - if (WINDOW.document && WINDOW.document.visibilityState === 'hidden') { + if (WINDOW.document?.visibilityState === 'hidden') { cb(); } else { handle = rIC(cb); diff --git a/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts b/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts index 7c8c1bb0b5c1..235895d093aa 100644 --- a/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts +++ b/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts @@ -30,9 +30,9 @@ export const TTFBThresholds: MetricRatingThresholds = [800, 1800]; * @param callback */ const whenReady = (callback: () => void) => { - if (WINDOW.document && WINDOW.document.prerendering) { + if (WINDOW.document?.prerendering) { whenActivated(() => whenReady(callback)); - } else if (WINDOW.document && WINDOW.document.readyState !== 'complete') { + } else if (WINDOW.document?.readyState !== 'complete') { addEventListener('load', () => whenReady(callback), true); } else { // Queue a task so the callback runs after `loadEventEnd`. diff --git a/packages/browser-utils/src/metrics/web-vitals/types/base.ts b/packages/browser-utils/src/metrics/web-vitals/types/base.ts index 6b612e7e22c5..846744d96da5 100644 --- a/packages/browser-utils/src/metrics/web-vitals/types/base.ts +++ b/packages/browser-utils/src/metrics/web-vitals/types/base.ts @@ -104,15 +104,6 @@ export type MetricWithAttribution = */ export type MetricRatingThresholds = [number, number]; -/** - * @deprecated Use metric-specific function types instead, such as: - * `(metric: LCPMetric) => void`. If a single callback type is needed for - * multiple metrics, use `(metric: MetricType) => void`. - */ -export interface ReportCallback { - (metric: MetricType): void; -} - export interface ReportOpts { reportAllChanges?: boolean; durationThreshold?: number; diff --git a/packages/browser-utils/test/utils/TestClient.ts b/packages/browser-utils/test/utils/TestClient.ts index 6e8f01c6d3e8..13f3ca82d3d6 100644 --- a/packages/browser-utils/test/utils/TestClient.ts +++ b/packages/browser-utils/test/utils/TestClient.ts @@ -1,4 +1,4 @@ -import { BaseClient, createTransport, initAndBind } from '@sentry/core'; +import { Client, createTransport, initAndBind } from '@sentry/core'; import { resolvedSyncPromise } from '@sentry/core'; import type { BrowserClientReplayOptions, @@ -10,7 +10,7 @@ import type { export interface TestClientOptions extends ClientOptions, BrowserClientReplayOptions {} -export class TestClient extends BaseClient { +export class TestClient extends Client { public constructor(options: TestClientOptions) { super(options); } diff --git a/packages/browser/package.json b/packages/browser/package.json index f588f2801eb0..23563cf5e752 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.18" + "node": ">=18" }, "files": [ "/build/npm" @@ -29,7 +29,7 @@ } }, "typesVersions": { - "<4.9": { + "<5.0": { "build/npm/types/index.d.ts": [ "build/npm/types-ts3.8/index.d.ts" ] diff --git a/packages/browser/rollup.bundle.config.mjs b/packages/browser/rollup.bundle.config.mjs index f65c27aad6e9..4567be0b297c 100644 --- a/packages/browser/rollup.bundle.config.mjs +++ b/packages/browser/rollup.bundle.config.mjs @@ -10,7 +10,6 @@ const reexportedPluggableIntegrationFiles = [ 'dedupe', 'extraerrordata', 'rewriteframes', - 'sessiontiming', 'feedback', 'modulemetadata', ]; @@ -37,6 +36,19 @@ reexportedPluggableIntegrationFiles.forEach(integrationName => { builds.push(...makeBundleConfigVariants(integrationsBundleConfig)); }); +// Bundle config for additional exports we don't want to include in the main SDK bundle +// if we need more of these, we can generalize the config as for pluggable integrations +builds.push( + ...makeBundleConfigVariants( + makeBaseBundleConfig({ + bundleType: 'addon', + entrypoints: ['src/pluggable-exports-bundle/index.multiplexedtransport.ts'], + licenseTitle: '@sentry/browser - multiplexedtransport', + outputFileBase: () => 'bundles/multiplexedtransport', + }), + ), +); + const baseBundleConfig = makeBaseBundleConfig({ bundleType: 'standalone', entrypoints: ['src/index.bundle.ts'], diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 2ce5c7dfece6..20b43ca6ddac 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -8,14 +8,11 @@ import type { ParameterizedString, Scope, SeverityLevel, - UserFeedback, } from '@sentry/core'; -import { BaseClient, applySdkMetadata, getSDKSource, logger } from '@sentry/core'; -import { DEBUG_BUILD } from './debug-build'; +import { Client, applySdkMetadata, getSDKSource } from '@sentry/core'; import { eventFromException, eventFromMessage } from './eventbuilder'; import { WINDOW } from './helpers'; import type { BrowserTransportOptions } from './transports/types'; -import { createUserFeedbackEnvelope } from './userfeedback'; /** * Configuration options for the Sentry Browser SDK. @@ -61,7 +58,7 @@ export type BrowserClientOptions = ClientOptions & * @see BrowserOptions for documentation on configuration options. * @see SentryClient for usage documentation. */ -export class BrowserClient extends BaseClient { +export class BrowserClient extends Client { /** * Creates a new Browser SDK instance. * @@ -85,6 +82,32 @@ export class BrowserClient extends BaseClient { } }); } + + if (this._options.sendDefaultPii) { + this.on('postprocessEvent', event => { + if (event.user?.ip_address === undefined) { + event.user = { + ...event.user, + ip_address: '{{auto}}', + }; + } + }); + + this.on('beforeSendSession', session => { + if ('aggregates' in session) { + if (session.attrs?.['ip_address'] === undefined) { + session.attrs = { + ...session.attrs, + ip_address: '{{auto}}', + }; + } + } else { + if (session.ipAddress === undefined) { + session.ipAddress = '{{auto}}'; + } + } + }); + } } /** @@ -105,33 +128,17 @@ export class BrowserClient extends BaseClient { return eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace); } - /** - * Sends user feedback to Sentry. - * - * @deprecated Use `captureFeedback` instead. - */ - public captureUserFeedback(feedback: UserFeedback): void { - if (!this._isEnabled()) { - DEBUG_BUILD && logger.warn('SDK not enabled, will not capture user feedback.'); - return; - } - - const envelope = createUserFeedbackEnvelope(feedback, { - metadata: this.getSdkMetadata(), - dsn: this.getDsn(), - tunnel: this.getOptions().tunnel, - }); - - // sendEnvelope should not throw - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendEnvelope(envelope); - } - /** * @inheritDoc */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + protected _prepareEvent( + event: Event, + hint: EventHint, + currentScope: Scope, + isolationScope: Scope, + ): PromiseLike { event.platform = event.platform || 'javascript'; - return super._prepareEvent(event, hint, scope); + + return super._prepareEvent(event, hint, currentScope, isolationScope); } } diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index ce34be0de707..acec653c5ee6 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -55,7 +55,7 @@ function eventFromPlainObject( isUnhandledRejection?: boolean, ): Event { const client = getClient(); - const normalizeDepth = client && client.getOptions().normalizeDepth; + const normalizeDepth = client?.getOptions().normalizeDepth; // If we can, we extract an exception from the object properties const errorFromProp = getErrorPropertyFromObject(exception); @@ -178,7 +178,7 @@ function isWebAssemblyException(exception: unknown): exception is WebAssembly.Ex * Usually, this is the `name` property on Error objects but WASM errors need to be treated differently. */ export function extractType(ex: Error & { message: { error?: Error } }): string | undefined { - const name = ex && ex.name; + const name = ex?.name; // The name for WebAssembly.Exception Errors needs to be extracted differently. // Context: https://github.com/getsentry/sentry-javascript/issues/13787 @@ -197,7 +197,7 @@ export function extractType(ex: Error & { message: { error?: Error } }): string * In this specific case we try to extract stacktrace.message.error.message */ export function extractMessage(ex: Error & { message: { error?: Error } }): string { - const message = ex && ex.message; + const message = ex?.message; if (!message) { return 'No error message'; @@ -225,11 +225,11 @@ export function eventFromException( hint?: EventHint, attachStacktrace?: boolean, ): PromiseLike { - const syntheticException = (hint && hint.syntheticException) || undefined; + const syntheticException = hint?.syntheticException || undefined; const event = eventFromUnknownInput(stackParser, exception, syntheticException, attachStacktrace); addExceptionMechanism(event); // defaults to { type: 'generic', handled: true } event.level = 'error'; - if (hint && hint.event_id) { + if (hint?.event_id) { event.event_id = hint.event_id; } return resolvedSyncPromise(event); @@ -246,10 +246,10 @@ export function eventFromMessage( hint?: EventHint, attachStacktrace?: boolean, ): PromiseLike { - const syntheticException = (hint && hint.syntheticException) || undefined; + const syntheticException = hint?.syntheticException || undefined; const event = eventFromString(stackParser, message, syntheticException, attachStacktrace); event.level = level; - if (hint && hint.event_id) { + if (hint?.event_id) { event.event_id = hint.event_id; } return resolvedSyncPromise(event); diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 492f9da23b38..ae2ff9c3fa87 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -1,8 +1,6 @@ export type { Breadcrumb, BreadcrumbHint, - // eslint-disable-next-line deprecation/deprecation - Request, RequestEventData, SdkInfo, Event, @@ -15,25 +13,23 @@ export type { Thread, User, Session, + ReportDialogOptions, } from '@sentry/core'; export type { BrowserOptions } from './client'; -export type { ReportDialogOptions } from './sdk'; - export { addEventProcessor, addBreadcrumb, addIntegration, captureException, captureEvent, + captureFeedback, captureMessage, close, createTransport, lastEventId, flush, - // eslint-disable-next-line deprecation/deprecation - getCurrentHub, getClient, isInitialized, getCurrentScope, @@ -62,6 +58,7 @@ export { spanToJSON, spanToTraceHeader, spanToBaggageHeader, + updateSpanName, } from '@sentry/core'; export { @@ -91,8 +88,6 @@ export { init, onLoad, showReportDialog, - // eslint-disable-next-line deprecation/deprecation - captureUserFeedback, } from './sdk'; export { breadcrumbsIntegration } from './integrations/breadcrumbs'; diff --git a/packages/browser/src/feedbackSync.ts b/packages/browser/src/feedbackSync.ts index b99c9a4b752f..ede41fefb221 100644 --- a/packages/browser/src/feedbackSync.ts +++ b/packages/browser/src/feedbackSync.ts @@ -3,11 +3,9 @@ import { feedbackModalIntegration, feedbackScreenshotIntegration, } from '@sentry-internal/feedback'; -import { lazyLoadIntegration } from './utils/lazyLoadIntegration'; /** Add a widget to capture user feedback to your application. */ export const feedbackSyncIntegration = buildFeedbackIntegration({ - lazyLoadIntegration, getModalIntegration: () => feedbackModalIntegration, getScreenshotIntegration: () => feedbackScreenshotIntegration, }); diff --git a/packages/browser/src/index.bundle.feedback.ts b/packages/browser/src/index.bundle.feedback.ts index c6f75c03d9d1..957583d79eeb 100644 --- a/packages/browser/src/index.bundle.feedback.ts +++ b/packages/browser/src/index.bundle.feedback.ts @@ -1,4 +1,4 @@ -import { browserTracingIntegrationShim, metricsShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; +import { browserTracingIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; import { feedbackAsyncIntegration } from './feedbackAsync'; export * from './index.bundle.base'; @@ -10,7 +10,6 @@ export { feedbackAsyncIntegration as feedbackAsyncIntegration, feedbackAsyncIntegration as feedbackIntegration, replayIntegrationShim as replayIntegration, - metricsShim as metrics, }; export { captureFeedback } from '@sentry/core'; diff --git a/packages/browser/src/index.bundle.replay.ts b/packages/browser/src/index.bundle.replay.ts index 2c1c8f0de424..86dc0fba7d25 100644 --- a/packages/browser/src/index.bundle.replay.ts +++ b/packages/browser/src/index.bundle.replay.ts @@ -1,8 +1,4 @@ -import { - browserTracingIntegrationShim, - feedbackIntegrationShim, - metricsShim, -} from '@sentry-internal/integration-shims'; +import { browserTracingIntegrationShim, feedbackIntegrationShim } from '@sentry-internal/integration-shims'; export * from './index.bundle.base'; @@ -12,5 +8,4 @@ export { browserTracingIntegrationShim as browserTracingIntegration, feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration, - metricsShim as metrics, }; diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index 6d86f90e01cc..a16f07bafaf2 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -4,8 +4,6 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; -export * from './metrics'; - export { getActiveSpan, getRootSpan, diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index a0fa6660b227..37f0da34ae25 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -4,8 +4,6 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; -export * from './metrics'; - export { getActiveSpan, getRootSpan, diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index 8115e628aa89..d540ff0bd6f9 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -5,8 +5,6 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; -export * from './metrics'; - export { getActiveSpan, getRootSpan, diff --git a/packages/browser/src/index.bundle.ts b/packages/browser/src/index.bundle.ts index 38787264f9b0..5004b376cd46 100644 --- a/packages/browser/src/index.bundle.ts +++ b/packages/browser/src/index.bundle.ts @@ -1,7 +1,6 @@ import { browserTracingIntegrationShim, feedbackIntegrationShim, - metricsShim, replayIntegrationShim, } from '@sentry-internal/integration-shims'; @@ -12,5 +11,4 @@ export { feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration, replayIntegrationShim as replayIntegration, - metricsShim as metrics, }; diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index e6f57c13fe6b..42c388d73547 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -6,12 +6,8 @@ export { contextLinesIntegration } from './integrations/contextlines'; export { captureConsoleIntegration, - // eslint-disable-next-line deprecation/deprecation - debugIntegration, extraErrorDataIntegration, rewriteFramesIntegration, - // eslint-disable-next-line deprecation/deprecation - sessionTimingIntegration, captureFeedback, } from '@sentry/core'; @@ -35,8 +31,6 @@ import { feedbackSyncIntegration } from './feedbackSync'; export { feedbackAsyncIntegration, feedbackSyncIntegration, feedbackSyncIntegration as feedbackIntegration }; export { getFeedback, sendFeedback } from '@sentry-internal/feedback'; -export * from './metrics'; - export { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './tracing/request'; export { browserTracingIntegration, @@ -45,8 +39,6 @@ export { } from './tracing/browserTracingIntegration'; export type { RequestInstrumentationOptions } from './tracing/request'; export { - // eslint-disable-next-line deprecation/deprecation - addTracingExtensions, registerSpanErrorInstrumentation, getActiveSpan, getRootSpan, @@ -75,3 +67,4 @@ export { } from './integrations/featureFlags'; export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly'; export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature'; +export { unleashIntegration } from './integrations/featureFlags/unleash'; diff --git a/packages/browser/src/integrations-bundle/index.debug.ts b/packages/browser/src/integrations-bundle/index.debug.ts index 7449888ce0ed..5539b5e36a6f 100644 --- a/packages/browser/src/integrations-bundle/index.debug.ts +++ b/packages/browser/src/integrations-bundle/index.debug.ts @@ -1,3 +1 @@ -// eslint-disable-next-line deprecation/deprecation -export { debugIntegration } from '@sentry/core'; export { spotlightBrowserIntegration } from '../integrations/spotlight'; diff --git a/packages/browser/src/integrations-bundle/index.sessiontiming.ts b/packages/browser/src/integrations-bundle/index.sessiontiming.ts deleted file mode 100644 index b601f2eb973b..000000000000 --- a/packages/browser/src/integrations-bundle/index.sessiontiming.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line deprecation/deprecation -export { sessionTimingIntegration } from '@sentry/core'; diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 5e31f5abe2a3..a45048ce2640 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -286,8 +286,12 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe return; } + const breadcrumbData: FetchBreadcrumbData = { + method: handlerData.fetchData.method, + url: handlerData.fetchData.url, + }; + if (handlerData.error) { - const data: FetchBreadcrumbData = handlerData.fetchData; const hint: FetchBreadcrumbHint = { data: handlerData.error, input: handlerData.args, @@ -298,7 +302,7 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe addBreadcrumb( { category: 'fetch', - data, + data: breadcrumbData, level: 'error', type: 'http', }, @@ -306,22 +310,23 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe ); } else { const response = handlerData.response as Response | undefined; - const data: FetchBreadcrumbData = { - ...handlerData.fetchData, - status_code: response && response.status, - }; + + breadcrumbData.request_body_size = handlerData.fetchData.request_body_size; + breadcrumbData.response_body_size = handlerData.fetchData.response_body_size; + breadcrumbData.status_code = response?.status; + const hint: FetchBreadcrumbHint = { input: handlerData.args, response, startTimestamp, endTimestamp, }; - const level = getBreadcrumbLogLevelFromHttpStatusCode(data.status_code); + const level = getBreadcrumbLogLevelFromHttpStatusCode(breadcrumbData.status_code); addBreadcrumb( { category: 'fetch', - data, + data: breadcrumbData, type: 'http', level, }, @@ -347,7 +352,7 @@ function _getHistoryBreadcrumbHandler(client: Client): (handlerData: HandlerData const parsedTo = parseUrl(to); // Initial pushState doesn't provide `from` information - if (!parsedFrom || !parsedFrom.path) { + if (!parsedFrom?.path) { parsedFrom = parsedLoc; } diff --git a/packages/browser/src/integrations/browserapierrors.ts b/packages/browser/src/integrations/browserapierrors.ts index dc0662500d7b..923079b62607 100644 --- a/packages/browser/src/integrations/browserapierrors.ts +++ b/packages/browser/src/integrations/browserapierrors.ts @@ -163,11 +163,10 @@ function _wrapXHR(originalSend: () => void): () => void { function _wrapEventTarget(target: string): void { const globalObject = WINDOW as unknown as Record; - const targetObj = globalObject[target]; - const proto = targetObj && targetObj.prototype; + const proto = globalObject[target]?.prototype; // eslint-disable-next-line no-prototype-builtins - if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) { + if (!proto?.hasOwnProperty?.('addEventListener')) { return; } diff --git a/packages/browser/src/integrations/contextlines.ts b/packages/browser/src/integrations/contextlines.ts index 66500e238614..775df33f0595 100644 --- a/packages/browser/src/integrations/contextlines.ts +++ b/packages/browser/src/integrations/contextlines.ts @@ -51,8 +51,8 @@ function addSourceContext(event: Event, contextLines: number): Event { return event; } - const exceptions = event.exception && event.exception.values; - if (!exceptions || !exceptions.length) { + const exceptions = event.exception?.values; + if (!exceptions?.length) { return event; } @@ -65,7 +65,7 @@ function addSourceContext(event: Event, contextLines: number): Event { exceptions.forEach(exception => { const stacktrace = exception.stacktrace; - if (stacktrace && stacktrace.frames) { + if (stacktrace?.frames) { stacktrace.frames = stacktrace.frames.map(frame => applySourceContextToFrame(frame, htmlLines, htmlFilename, contextLines), ); diff --git a/packages/browser/src/integrations/featureFlags/unleash/index.ts b/packages/browser/src/integrations/featureFlags/unleash/index.ts new file mode 100644 index 000000000000..934ff196ee95 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/unleash/index.ts @@ -0,0 +1 @@ +export { unleashIntegration } from './integration'; diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts new file mode 100644 index 000000000000..c451afb831ba --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -0,0 +1,73 @@ +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; + +import { defineIntegration, fill, logger } from '@sentry/core'; +import { DEBUG_BUILD } from '../../../debug-build'; +import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import type { UnleashClient, UnleashClientClass } from './types'; + +/** + * Sentry integration for capturing feature flag evaluations from the Unleash SDK. + * + * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. + * + * @example + * ``` + * import { UnleashClient } from 'unleash-proxy-client'; + * import * as Sentry from '@sentry/browser'; + * + * Sentry.init({ + * dsn: '___PUBLIC_DSN___', + * integrations: [Sentry.unleashIntegration({unleashClientClass: UnleashClient})], + * }); + * + * const unleash = new UnleashClient(...); + * unleash.start(); + * + * unleash.isEnabled('my-feature'); + * unleash.getVariant('other-feature'); + * Sentry.captureException(new Error('something went wrong')); + * ``` + */ +export const unleashIntegration = defineIntegration( + ({ unleashClientClass }: { unleashClientClass: UnleashClientClass }) => { + return { + name: 'Unleash', + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return copyFlagsFromScopeToEvent(event); + }, + + setupOnce() { + const unleashClientPrototype = unleashClientClass.prototype as UnleashClient; + fill(unleashClientPrototype, 'isEnabled', _wrappedIsEnabled); + }, + }; + }, +) satisfies IntegrationFn; + +/** + * Wraps the UnleashClient.isEnabled method to capture feature flag evaluations. Its only side effect is writing to Sentry scope. + * + * This wrapper is safe for all isEnabled signatures. If the signature does not match (this: UnleashClient, toggleName: string, ...args: unknown[]) => boolean, + * we log an error and return the original result. + * + * @param original - The original method. + * @returns Wrapped method. Results should match the original. + */ +function _wrappedIsEnabled( + original: (this: UnleashClient, ...args: unknown[]) => unknown, +): (this: UnleashClient, ...args: unknown[]) => unknown { + return function (this: UnleashClient, ...args: unknown[]): unknown { + const toggleName = args[0]; + const result = original.apply(this, args); + + if (typeof toggleName === 'string' && typeof result === 'boolean') { + insertFlagToScope(toggleName, result); + } else if (DEBUG_BUILD) { + logger.error( + `[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: ${toggleName} (${typeof toggleName}), result: ${result} (${typeof result})`, + ); + } + return result; + }; +} diff --git a/packages/browser/src/integrations/featureFlags/unleash/types.ts b/packages/browser/src/integrations/featureFlags/unleash/types.ts new file mode 100644 index 000000000000..c87798859911 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/unleash/types.ts @@ -0,0 +1,23 @@ +export interface IVariant { + name: string; + enabled: boolean; + feature_enabled?: boolean; + payload?: { + type: string; + value: string; + }; +} + +export interface UnleashClient { + isEnabled(this: UnleashClient, featureName: string): boolean; + getVariant(this: UnleashClient, featureName: string): IVariant; +} + +export interface IConfig { + [key: string]: unknown; + appName: string; + clientKey: string; + url: URL | string; +} + +export type UnleashClientClass = new (config: IConfig) => UnleashClient; diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index abb768082c3a..21e7440b0bc3 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -194,7 +194,7 @@ function globalHandlerLog(type: string): void { function getOptions(): { stackParser: StackParser; attachStacktrace?: boolean } { const client = getClient(); - const options = (client && client.getOptions()) || { + const options = client?.getOptions() || { stackParser: () => [], attachStacktrace: false, }; diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts index 0db88cfe355c..78e27713c78f 100644 --- a/packages/browser/src/integrations/httpcontext.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -1,4 +1,4 @@ -import { defineIntegration } from '@sentry/core'; +import { defineIntegration, getLocationHref } from '@sentry/core'; import { WINDOW } from '../helpers'; /** @@ -15,16 +15,20 @@ export const httpContextIntegration = defineIntegration(() => { } // grab as much info as exists and add it to the event - const url = (event.request && event.request.url) || (WINDOW.location && WINDOW.location.href); + const url = event.request?.url || getLocationHref(); const { referrer } = WINDOW.document || {}; const { userAgent } = WINDOW.navigator || {}; const headers = { - ...(event.request && event.request.headers), + ...event.request?.headers, ...(referrer && { Referer: referrer }), ...(userAgent && { 'User-Agent': userAgent }), }; - const request = { ...event.request, ...(url && { url }), headers }; + const request = { + ...event.request, + ...(url && { url }), + headers, + }; event.request = request; }, diff --git a/packages/browser/src/integrations/spotlight.ts b/packages/browser/src/integrations/spotlight.ts index 7d3cc61d5015..c18457ba150f 100644 --- a/packages/browser/src/integrations/spotlight.ts +++ b/packages/browser/src/integrations/spotlight.ts @@ -84,6 +84,6 @@ export function isSpotlightInteraction(event: Event): boolean { event.contexts && event.contexts.trace && event.contexts.trace.op === 'ui.action.click' && - event.spans.some(({ description }) => description && description.includes('#sentry-spotlight')), + event.spans.some(({ description }) => description?.includes('#sentry-spotlight')), ); } diff --git a/packages/browser/src/metrics.ts b/packages/browser/src/metrics.ts deleted file mode 100644 index 96f56988c485..000000000000 --- a/packages/browser/src/metrics.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { BrowserMetricsAggregator, metrics as metricsCore } from '@sentry/core'; -import type { DurationUnit, MetricData, Metrics } from '@sentry/core'; - -/** - * Adds a value to a counter metric - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function increment(name: string, value: number = 1, data?: MetricData): void { - // eslint-disable-next-line deprecation/deprecation - metricsCore.increment(BrowserMetricsAggregator, name, value, data); -} - -/** - * Adds a value to a distribution metric - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function distribution(name: string, value: number, data?: MetricData): void { - // eslint-disable-next-line deprecation/deprecation - metricsCore.distribution(BrowserMetricsAggregator, name, value, data); -} - -/** - * Adds a value to a set metric. Value must be a string or integer. - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function set(name: string, value: number | string, data?: MetricData): void { - // eslint-disable-next-line deprecation/deprecation - metricsCore.set(BrowserMetricsAggregator, name, value, data); -} - -/** - * Adds a value to a gauge metric - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function gauge(name: string, value: number, data?: MetricData): void { - // eslint-disable-next-line deprecation/deprecation - metricsCore.gauge(BrowserMetricsAggregator, name, value, data); -} - -/** - * Adds a timing metric. - * The metric is added as a distribution metric. - * - * You can either directly capture a numeric `value`, or wrap a callback function in `timing`. - * In the latter case, the duration of the callback execution will be captured as a span & a metric. - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function timing(name: string, value: number, unit?: DurationUnit, data?: Omit): void; -function timing(name: string, callback: () => T, unit?: DurationUnit, data?: Omit): T; -function timing( - name: string, - value: number | (() => T), - unit: DurationUnit = 'second', - data?: Omit, -): T | void { - // eslint-disable-next-line deprecation/deprecation - return metricsCore.timing(BrowserMetricsAggregator, name, value, unit, data); -} - -/** - * The metrics API is used to capture custom metrics in Sentry. - * - * @deprecated The Sentry metrics beta has ended. This export will be removed in a future release. - */ -export const metrics: Metrics = { - increment, - distribution, - set, - gauge, - timing, -}; diff --git a/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts b/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts new file mode 100644 index 000000000000..a7d637d9e62f --- /dev/null +++ b/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts @@ -0,0 +1 @@ +export { makeMultiplexedTransport } from '@sentry/core'; diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index b0ca69a37b00..df3c4923d1c9 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -49,9 +49,9 @@ const _browserProfilingIntegration = (() => { const profilesToAddToEnvelope: Profile[] = []; for (const profiledTransaction of profiledTransactionEvents) { - const context = profiledTransaction && profiledTransaction.contexts; - const profile_id = context && context['profile'] && context['profile']['profile_id']; - const start_timestamp = context && context['profile'] && context['profile']['start_timestamp']; + const context = profiledTransaction?.contexts; + const profile_id = context?.profile?.['profile_id']; + const start_timestamp = context?.profile?.['start_timestamp']; if (typeof profile_id !== 'string') { DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a span without a profile context'); @@ -64,7 +64,7 @@ const _browserProfilingIntegration = (() => { } // Remove the profile from the span context before sending, relay will take care of the rest. - if (context && context['profile']) { + if (context?.profile) { delete context.profile; } diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 21cbadb58176..04661e0bbb1a 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -21,16 +21,16 @@ const MS_TO_NS = 1e6; const THREAD_ID_STRING = String(0); const THREAD_NAME = 'main'; +// We force make this optional to be on the safe side... +const navigator = WINDOW.navigator as typeof WINDOW.navigator | undefined; + // Machine properties (eval only once) let OS_PLATFORM = ''; let OS_PLATFORM_VERSION = ''; let OS_ARCH = ''; -let OS_BROWSER = (WINDOW.navigator && WINDOW.navigator.userAgent) || ''; +let OS_BROWSER = navigator?.userAgent || ''; let OS_MODEL = ''; -const OS_LOCALE = - (WINDOW.navigator && WINDOW.navigator.language) || - (WINDOW.navigator && WINDOW.navigator.languages && WINDOW.navigator.languages[0]) || - ''; +const OS_LOCALE = navigator?.language || navigator?.languages?.[0] || ''; type UAData = { platform?: string; @@ -52,7 +52,7 @@ function isUserAgentData(data: unknown): data is UserAgentData { } // @ts-expect-error userAgentData is not part of the navigator interface yet -const userAgentData = WINDOW.navigator && WINDOW.navigator.userAgentData; +const userAgentData = navigator?.userAgentData; if (isUserAgentData(userAgentData)) { userAgentData @@ -63,7 +63,7 @@ if (isUserAgentData(userAgentData)) { OS_MODEL = ua.model || ''; OS_PLATFORM_VERSION = ua.platformVersion || ''; - if (ua.fullVersionList && ua.fullVersionList.length > 0) { + if (ua.fullVersionList?.length) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const firstUa = ua.fullVersionList[ua.fullVersionList.length - 1]!; OS_BROWSER = `${firstUa.brand} ${firstUa.version}`; @@ -98,7 +98,7 @@ export interface ProfiledEvent extends Event { } function getTraceId(event: Event): string { - const traceId: unknown = event && event.contexts && event.contexts['trace'] && event.contexts['trace']['trace_id']; + const traceId: unknown = event.contexts?.trace?.['trace_id']; // Log a warning if the profile has an invalid traceId (should be uuidv4). // All profiles and transactions are rejected if this is the case and we want to // warn users that this is happening if they enable debug flag @@ -199,7 +199,7 @@ export function createProfilePayload( * */ export function isProfiledTransactionEvent(event: Event): event is ProfiledEvent { - return !!(event.sdkProcessingMetadata && event.sdkProcessingMetadata['profile']); + return !!event.sdkProcessingMetadata?.profile; } /* @@ -241,9 +241,9 @@ export function convertJSSelfProfileToSampledFormat(input: JSSelfProfile): Profi // when that happens, we need to ensure we are correcting the profile timings so the two timelines stay in sync. // Since JS self profiling time origin is always initialized to performance.timeOrigin, we need to adjust for // the drift between the SDK selected value and our profile time origin. - const origin = - typeof performance.timeOrigin === 'number' ? performance.timeOrigin : browserPerformanceTimeOrigin || 0; - const adjustForOriginChange = origin - (browserPerformanceTimeOrigin || origin); + const perfOrigin = browserPerformanceTimeOrigin(); + const origin = typeof performance.timeOrigin === 'number' ? performance.timeOrigin : perfOrigin || 0; + const adjustForOriginChange = origin - (perfOrigin || origin); input.samples.forEach((jsSample, i) => { // If sample has no stack, add an empty sample @@ -333,7 +333,7 @@ export function findProfiledTransactionsFromEnvelope(envelope: Envelope): Event[ for (let j = 1; j < item.length; j++) { const event = item[j] as Event; - if (event && event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id']) { + if (event?.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id']) { events.push(item[j] as Event); } } @@ -347,8 +347,8 @@ export function findProfiledTransactionsFromEnvelope(envelope: Envelope): Event[ */ export function applyDebugMetadata(resource_paths: ReadonlyArray): DebugImage[] { const client = getClient(); - const options = client && client.getOptions(); - const stackParser = options && options.stackParser; + const options = client?.getOptions(); + const stackParser = options?.stackParser; if (!stackParser) { return []; @@ -478,7 +478,7 @@ export function shouldProfileSpan(span: Span): boolean { } const client = getClient(); - const options = client && client.getOptions(); + const options = client?.getOptions(); if (!options) { DEBUG_BUILD && logger.log('[Profiling] Profiling disabled, no options found.'); return false; diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 425212015455..d6fedeba769b 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,10 +1,11 @@ +import type { Client, Integration, Options, ReportDialogOptions } from '@sentry/core'; import { consoleSandbox, dedupeIntegration, functionToStringIntegration, - getClient, getCurrentScope, getIntegrationsToSetup, + getLocationHref, getReportDialogEndpoint, inboundFiltersIntegration, initAndBind, @@ -13,7 +14,6 @@ import { stackParserFromStackParserOptions, supportsFetch, } from '@sentry/core'; -import type { Client, DsnLike, Integration, Options, UserFeedback } from '@sentry/core'; import type { BrowserClientOptions, BrowserOptions } from './client'; import { BrowserClient } from './client'; import { DEBUG_BUILD } from './debug-build'; @@ -28,12 +28,12 @@ import { defaultStackParser } from './stack-parsers'; import { makeFetchTransport } from './transports/fetch'; /** Get the default integrations for the browser SDK. */ -export function getDefaultIntegrations(options: Options): Integration[] { +export function getDefaultIntegrations(_options: Options): Integration[] { /** * Note: Please make sure this stays in sync with Angular SDK, which re-exports * `getDefaultIntegrations` but with an adjusted set of integrations. */ - const integrations = [ + return [ inboundFiltersIntegration(), functionToStringIntegration(), browserApiErrorsIntegration(), @@ -42,38 +42,44 @@ export function getDefaultIntegrations(options: Options): Integration[] { linkedErrorsIntegration(), dedupeIntegration(), httpContextIntegration(), + browserSessionIntegration(), ]; - - // eslint-disable-next-line deprecation/deprecation - if (options.autoSessionTracking !== false) { - integrations.push(browserSessionIntegration()); - } - - return integrations; } -function applyDefaultOptions(optionsArg: BrowserOptions = {}): BrowserOptions { +/** Exported only for tests. */ +export function applyDefaultOptions(optionsArg: BrowserOptions = {}): BrowserOptions { const defaultOptions: BrowserOptions = { defaultIntegrations: getDefaultIntegrations(optionsArg), release: typeof __SENTRY_RELEASE__ === 'string' // This allows build tooling to find-and-replace __SENTRY_RELEASE__ to inject a release value ? __SENTRY_RELEASE__ - : WINDOW.SENTRY_RELEASE && WINDOW.SENTRY_RELEASE.id // This supports the variable that sentry-webpack-plugin injects + : WINDOW.SENTRY_RELEASE?.id // This supports the variable that sentry-webpack-plugin injects ? WINDOW.SENTRY_RELEASE.id : undefined, - autoSessionTracking: true, sendClientReports: true, }; - // TODO: Instead of dropping just `defaultIntegrations`, we should simply - // call `dropUndefinedKeys` on the entire `optionsArg`. - // However, for this to work we need to adjust the `hasTracingEnabled()` logic - // first as it differentiates between `undefined` and the key not being in the object. - if (optionsArg.defaultIntegrations == null) { - delete optionsArg.defaultIntegrations; + return { + ...defaultOptions, + ...dropTopLevelUndefinedKeys(optionsArg), + }; +} + +/** + * In contrast to the regular `dropUndefinedKeys` method, + * this one does not deep-drop keys, but only on the top level. + */ +function dropTopLevelUndefinedKeys(obj: T): Partial { + const mutatetedObj: Partial = {}; + + for (const k of Object.getOwnPropertyNames(obj)) { + const key = k as keyof T; + if (obj[key] !== undefined) { + mutatetedObj[key] = obj[key]; + } } - return { ...defaultOptions, ...optionsArg }; + return mutatetedObj; } type ExtensionProperties = { @@ -98,8 +104,8 @@ function shouldShowBrowserExtensionError(): boolean { const extensionKey = windowWithMaybeExtension.chrome ? 'chrome' : 'browser'; const extensionObject = windowWithMaybeExtension[extensionKey]; - const runtimeId = extensionObject && extensionObject.runtime && extensionObject.runtime.id; - const href = (WINDOW.location && WINDOW.location.href) || ''; + const runtimeId = extensionObject?.runtime?.id; + const href = getLocationHref() || ''; const extensionProtocols = ['chrome-extension:', 'moz-extension:', 'ms-browser-extension:', 'safari-web-extension:']; @@ -195,37 +201,6 @@ export function init(browserOptions: BrowserOptions = {}): Client | undefined { return initAndBind(BrowserClient, clientOptions); } -/** - * All properties the report dialog supports - */ -export interface ReportDialogOptions { - // TODO(v9): Change this to [key: string]: unknkown; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; - eventId?: string; - dsn?: DsnLike; - user?: { - email?: string; - name?: string; - }; - lang?: string; - title?: string; - subtitle?: string; - subtitle2?: string; - labelName?: string; - labelEmail?: string; - labelComments?: string; - labelClose?: string; - labelSubmit?: string; - errorGeneric?: string; - errorFormEntry?: string; - successMessage?: string; - /** Callback after reportDialog showed up */ - onLoad?(this: void): void; - /** Callback after reportDialog closed */ - onClose?(this: void): void; -} - /** * Present the user with a report dialog. * @@ -240,7 +215,7 @@ export function showReportDialog(options: ReportDialogOptions = {}): void { const scope = getCurrentScope(); const client = scope.getClient(); - const dsn = client && client.getDsn(); + const dsn = client?.getDsn(); if (!dsn) { DEBUG_BUILD && logger.error('DSN not configured for showReportDialog call'); @@ -307,16 +282,3 @@ export function forceLoad(): void { export function onLoad(callback: () => void): void { callback(); } - -/** - * Captures user feedback and sends it to Sentry. - * - * @deprecated Use `captureFeedback` instead. - */ -export function captureUserFeedback(feedback: UserFeedback): void { - const client = getClient(); - if (client) { - // eslint-disable-next-line deprecation/deprecation - client.captureUserFeedback(feedback); - } -} diff --git a/packages/browser/src/tracing/backgroundtab.ts b/packages/browser/src/tracing/backgroundtab.ts index 3a591bde2b8e..1eab49e0d0fd 100644 --- a/packages/browser/src/tracing/backgroundtab.ts +++ b/packages/browser/src/tracing/backgroundtab.ts @@ -7,7 +7,7 @@ import { WINDOW } from '../helpers'; * document is hidden. */ export function registerBackgroundTabDetection(): void { - if (WINDOW && WINDOW.document) { + if (WINDOW.document) { WINDOW.document.addEventListener('visibilitychange', () => { const activeSpan = getActiveSpan(); if (!activeSpan) { diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 17030f2f4a43..f34a37542d29 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -16,15 +16,14 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, TRACING_DEFAULTS, + addNonEnumerableProperty, browserPerformanceTimeOrigin, generateTraceId, - getActiveSpan, getClient, getCurrentScope, - getDomElement, getDynamicSamplingContextFromSpan, getIsolationScope, - getRootSpan, + getLocationHref, logger, propagationContextFromHeaders, registerSpanErrorInstrumentation, @@ -190,6 +189,12 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { * We explicitly export the proper type here, as this has to be extended in some cases. */ export const browserTracingIntegration = ((_options: Partial = {}) => { + /** + * This is just a small wrapper that makes `document` optional. + * We want to be extra-safe and always check that this exists, to ensure weird environments do not blow up. + */ + const optionalWindowDocument = WINDOW.document as (typeof WINDOW)['document'] | undefined; + registerSpanErrorInstrumentation(); const { @@ -241,7 +246,7 @@ export const browserTracingIntegration = ((_options: Partial { _collectWebVitals(); addPerformanceEntries(span, { recordClsOnPageloadSpan: !enableStandaloneClsSpans }); + setActiveIdleSpan(client, undefined); + + // A trace should stay consistent over the entire timespan of one route - even after the pageload/navigation ended. + // Only when another navigation happens, we want to create a new trace. + // This way, e.g. errors that occur after the pageload span ended are still associated to the pageload trace. + const scope = getCurrentScope(); + const oldPropagationContext = scope.getPropagationContext(); + + scope.setPropagationContext({ + ...oldPropagationContext, + traceId: idleSpan.spanContext().traceId, + sampled: spanIsSampled(idleSpan), + dsc: getDynamicSamplingContextFromSpan(span), + }); }, }); + setActiveIdleSpan(client, idleSpan); function emitFinish(): void { - if (['interactive', 'complete'].includes(WINDOW.document.readyState)) { + if (optionalWindowDocument && ['interactive', 'complete'].includes(optionalWindowDocument.readyState)) { client.emit('idleSpanEnableAutoFinish', idleSpan); } } - if (isPageloadTransaction && WINDOW.document) { - WINDOW.document.addEventListener('readystatechange', () => { + if (isPageloadTransaction && optionalWindowDocument) { + optionalWindowDocument.addEventListener('readystatechange', () => { emitFinish(); }); emitFinish(); } - - return idleSpan; } return { name: BROWSER_TRACING_INTEGRATION_ID, afterAllSetup(client) { - let activeSpan: Span | undefined; - let startingUrl: string | undefined = WINDOW.location && WINDOW.location.href; + let startingUrl: string | undefined = getLocationHref(); function maybeEndActiveSpan(): void { + const activeSpan = getActiveIdleSpan(client); + if (activeSpan && !spanToJSON(activeSpan).timestamp) { DEBUG_BUILD && logger.log(`[Tracing] Finishing current active span with op: ${spanToJSON(activeSpan).op}`); // If there's an open active span, we need to finish it before creating an new one. @@ -310,7 +329,10 @@ export const browserTracingIntegration = ((_options: Partial { - const op = spanToJSON(span).op; - if (span !== getRootSpan(span) || (op !== 'navigation' && op !== 'pageload')) { - return; - } - - const scope = getCurrentScope(); - const oldPropagationContext = scope.getPropagationContext(); - - scope.setPropagationContext({ - ...oldPropagationContext, - sampled: oldPropagationContext.sampled !== undefined ? oldPropagationContext.sampled : spanIsSampled(span), - dsc: oldPropagationContext.dsc || getDynamicSamplingContextFromSpan(span), - }); - }); - if (WINDOW.location) { if (instrumentPageLoad) { + const origin = browserPerformanceTimeOrigin(); startBrowserTracingPageLoadSpan(client, { name: WINDOW.location.pathname, // pageload should always start at timeOrigin (and needs to be in s, not ms) - startTime: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined, + startTime: origin ? origin / 1000 : undefined, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', @@ -379,7 +381,7 @@ export const browserTracingIntegration = ((_options: Partial { const op = 'ui.action.click'; - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - if (rootSpan) { - const currentRootSpanOp = spanToJSON(rootSpan).op; + const activeIdleSpan = getActiveIdleSpan(client); + if (activeIdleSpan) { + const currentRootSpanOp = spanToJSON(activeIdleSpan).op; if (['navigation', 'pageload'].includes(currentRootSpanOp as string)) { DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} span because a pageload or navigation span is in progress.`); @@ -519,7 +521,17 @@ function registerInteractionListener( ); }; - if (WINDOW.document) { + if (optionalWindowDocument) { addEventListener('click', registerInteractionTransaction, { once: false, capture: true }); } } + +// We store the active idle span on the client object, so we can access it from exported functions +const ACTIVE_IDLE_SPAN_PROPERTY = '_sentry_idleSpan'; +function getActiveIdleSpan(client: Client): Span | undefined { + return (client as { [ACTIVE_IDLE_SPAN_PROPERTY]?: Span })[ACTIVE_IDLE_SPAN_PROPERTY]; +} + +function setActiveIdleSpan(client: Client, span: Span | undefined): void { + addNonEnumerableProperty(client, ACTIVE_IDLE_SPAN_PROPERTY, span); +} diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 5f32b227fa85..92a8f2924084 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -12,6 +12,7 @@ import { addFetchInstrumentationHandler, browserPerformanceTimeOrigin, getActiveSpan, + getLocationHref, getTraceData, hasTracingEnabled, instrumentFetchRequest, @@ -208,7 +209,7 @@ function isPerformanceResourceTiming(entry: PerformanceEntry): entry is Performa * @param span A span that has yet to be finished, must contain `url` on data. */ function addHTTPTimings(span: Span): void { - const { url } = spanToJSON(span).data || {}; + const { url } = spanToJSON(span).data; if (!url || typeof url !== 'string') { return; @@ -259,7 +260,7 @@ export function extractNetworkProtocol(nextHopProtocol: string): { name: string; } function getAbsoluteTime(time: number = 0): number { - return ((browserPerformanceTimeOrigin || performance.timeOrigin) + time) / 1000; + return ((browserPerformanceTimeOrigin() || performance.timeOrigin) + time) / 1000; } function resourceTimingEntryToSpanData(resourceTiming: PerformanceResourceTiming): [string, string | number][] { @@ -269,7 +270,7 @@ function resourceTimingEntryToSpanData(resourceTiming: PerformanceResourceTiming timingSpanData.push(['network.protocol.version', version], ['network.protocol.name', name]); - if (!browserPerformanceTimeOrigin) { + if (!browserPerformanceTimeOrigin()) { return timingSpanData; } return [ @@ -297,7 +298,7 @@ export function shouldAttachHeaders( ): boolean { // window.location.href not being defined is an edge case in the browser but we need to handle it. // Potentially dangerous situations where it may not be defined: Browser Extensions, Web Workers, patching of the location obj - const href: string | undefined = WINDOW.location && WINDOW.location.href; + const href = getLocationHref(); if (!href) { // If there is no window.location.origin, we default to only attaching tracing headers to relative requests, i.e. ones that start with `/` @@ -345,7 +346,7 @@ export function xhrCallback( spans: Record, ): Span | undefined { const xhr = handlerData.xhr; - const sentryXhrData = xhr && xhr[SENTRY_XHR_DATA_KEY]; + const sentryXhrData = xhr?.[SENTRY_XHR_DATA_KEY]; if (!xhr || xhr.__sentry_own_request__ || !sentryXhrData) { return undefined; diff --git a/packages/browser/src/transports/offline.ts b/packages/browser/src/transports/offline.ts index 372c360194c7..5fbf7fa6ffc4 100644 --- a/packages/browser/src/transports/offline.ts +++ b/packages/browser/src/transports/offline.ts @@ -1,5 +1,6 @@ import type { BaseTransportOptions, Envelope, OfflineStore, OfflineTransportOptions, Transport } from '@sentry/core'; import { makeOfflineTransport, parseEnvelope, serializeEnvelope } from '@sentry/core'; +import { WINDOW } from '../helpers'; import { makeFetchTransport } from './fetch'; // 'Store', 'promisifyRequest' and 'createStore' were originally copied from the 'idb-keyval' package before being @@ -158,7 +159,15 @@ function createIndexedDbStore(options: BrowserOfflineTransportOptions): OfflineS function makeIndexedDbOfflineTransport( createTransport: (options: T) => Transport, ): (options: T & BrowserOfflineTransportOptions) => Transport { - return options => createTransport({ ...options, createStore: createIndexedDbStore }); + return options => { + const transport = createTransport({ ...options, createStore: createIndexedDbStore }); + + WINDOW.addEventListener('online', async _ => { + await transport.flush(); + }); + + return transport; + }; } /** diff --git a/packages/browser/src/userfeedback.ts b/packages/browser/src/userfeedback.ts index dcafa6c3c98c..57e42eaafb9a 100644 --- a/packages/browser/src/userfeedback.ts +++ b/packages/browser/src/userfeedback.ts @@ -19,13 +19,12 @@ export function createUserFeedbackEnvelope( const headers: EventEnvelope[0] = { event_id: feedback.event_id, sent_at: new Date().toISOString(), - ...(metadata && - metadata.sdk && { - sdk: { - name: metadata.sdk.name, - version: metadata.sdk.version, - }, - }), + ...(metadata?.sdk && { + sdk: { + name: metadata.sdk.name, + version: metadata.sdk.version, + }, + }), ...(!!tunnel && !!dsn && { dsn: dsnToString(dsn) }), }; const item = createUserFeedbackEnvelopeItem(feedback); diff --git a/packages/browser/src/utils/lazyLoadIntegration.ts b/packages/browser/src/utils/lazyLoadIntegration.ts index 768431316cdd..2e215fbf764e 100644 --- a/packages/browser/src/utils/lazyLoadIntegration.ts +++ b/packages/browser/src/utils/lazyLoadIntegration.ts @@ -13,13 +13,11 @@ const LazyLoadableIntegrations = { captureConsoleIntegration: 'captureconsole', contextLinesIntegration: 'contextlines', linkedErrorsIntegration: 'linkederrors', - debugIntegration: 'debug', dedupeIntegration: 'dedupe', extraErrorDataIntegration: 'extraerrordata', httpClientIntegration: 'httpclient', reportingObserverIntegration: 'reportingobserver', rewriteFramesIntegration: 'rewriteframes', - sessionTimingIntegration: 'sessiontiming', browserProfilingIntegration: 'browserprofiling', moduleMetadataIntegration: 'modulemetadata', } as const; @@ -70,7 +68,7 @@ export async function lazyLoadIntegration( }); const currentScript = WINDOW.document.currentScript; - const parent = WINDOW.document.body || WINDOW.document.head || (currentScript && currentScript.parentElement); + const parent = WINDOW.document.body || WINDOW.document.head || currentScript?.parentElement; if (parent) { parent.appendChild(script); @@ -95,8 +93,7 @@ export async function lazyLoadIntegration( function getScriptURL(bundle: string): string { const client = getClient(); - const options = client && client.getOptions(); - const baseURL = (options && options.cdnBaseUrl) || 'https://browser.sentry-cdn.com'; + const baseURL = client?.getOptions()?.cdnBaseUrl || 'https://browser.sentry-cdn.com'; return new URL(`/${SDK_VERSION}/${bundle}.min.js`, baseURL).toString(); } diff --git a/packages/browser/test/loader.js b/packages/browser/test/loader.js index cfb749ae50a8..5361aea71b7a 100644 --- a/packages/browser/test/loader.js +++ b/packages/browser/test/loader.js @@ -37,8 +37,8 @@ if ( ('e' in content || 'p' in content || - (content.f && content.f.indexOf('capture') > -1) || - (content.f && content.f.indexOf('showReportDialog') > -1)) && + (content.f?.indexOf('capture') > -1) || + (content.f?.indexOf('showReportDialog') > -1)) && lazy ) { // We only want to lazy inject/load the sdk bundle if @@ -115,7 +115,7 @@ var initAlreadyCalled = false; var __sentry = _window['__SENTRY__']; // If there is a global __SENTRY__ that means that in any of the callbacks init() was already invoked - if (!(typeof __sentry === 'undefined') && __sentry.hub && __sentry.hub.getClient()) { + if (!(typeof __sentry === 'undefined') && __sentry.hub?.getClient()) { initAlreadyCalled = true; } diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index e00974ab0f5d..a6fc49edee89 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -7,13 +7,13 @@ import type { Mock } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as SentryCore from '@sentry/core'; -import { Scope, createTransport } from '@sentry/core'; +import { createTransport } from '@sentry/core'; import { resolvedSyncPromise } from '@sentry/core'; -import type { Client, Integration } from '@sentry/core'; +import type { Integration } from '@sentry/core'; import type { BrowserOptions } from '../src'; import { WINDOW } from '../src'; -import { init } from '../src/sdk'; +import { applyDefaultOptions, getDefaultIntegrations, init } from '../src/sdk'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -34,30 +34,6 @@ export class MockIntegration implements Integration { } } -vi.mock('@sentry/core', async requireActual => { - return { - ...((await requireActual()) as any), - getCurrentHub(): { - bindClient(client: Client): boolean; - getClient(): boolean; - getScope(): Scope; - } { - return { - getClient(): boolean { - return false; - }, - getScope(): Scope { - return new Scope(); - }, - bindClient(client: Client): boolean { - client.init!(); - return true; - }, - }; - }, - }; -}); - describe('init', () => { beforeEach(() => { vi.clearAllMocks(); @@ -89,7 +65,7 @@ describe('init', () => { expect(initAndBindSpy).toHaveBeenCalledTimes(1); const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; - expect(optionsPassed?.integrations?.length).toBeGreaterThan(0); + expect(optionsPassed?.integrations.length).toBeGreaterThan(0); }); test("doesn't install default integrations if told not to", () => { @@ -154,8 +130,6 @@ describe('init', () => { new MockIntegration('MockIntegration 0.2'), ]; - const originalLocation = WINDOW.location || {}; - const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: DEFAULT_INTEGRATIONS }); afterEach(() => { @@ -204,12 +178,9 @@ describe('init', () => { extensionProtocol => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - // @ts-expect-error - this is a hack to simulate a dedicated page in a browser extension - delete WINDOW.location; - // @ts-expect-error - this is a hack to simulate a dedicated page in a browser extension - WINDOW.location = { - href: `${extensionProtocol}://mock-extension-id/dedicated-page.html`, - }; + const locationHrefSpy = vi + .spyOn(SentryCore, 'getLocationHref') + .mockImplementation(() => `${extensionProtocol}://mock-extension-id/dedicated-page.html`); Object.defineProperty(WINDOW, 'browser', { value: { runtime: { id: 'mock-extension-id' } }, writable: true }); @@ -218,7 +189,7 @@ describe('init', () => { expect(consoleErrorSpy).toBeCalledTimes(0); consoleErrorSpy.mockRestore(); - WINDOW.location = originalLocation; + locationHrefSpy.mockRestore(); }, ); @@ -277,3 +248,97 @@ describe('init', () => { expect(client).not.toBeUndefined(); }); }); + +describe('applyDefaultOptions', () => { + test('it works with empty options', () => { + const options = {}; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + defaultIntegrations: expect.any(Array), + release: undefined, + sendClientReports: true, + }); + + expect((actual.defaultIntegrations as { name: string }[]).map(i => i.name)).toEqual( + getDefaultIntegrations(options).map(i => i.name), + ); + }); + + test('it works with options', () => { + const options = { + tracesSampleRate: 0.5, + release: '1.0.0', + }; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + defaultIntegrations: expect.any(Array), + release: '1.0.0', + sendClientReports: true, + tracesSampleRate: 0.5, + }); + + expect((actual.defaultIntegrations as { name: string }[]).map(i => i.name)).toEqual( + getDefaultIntegrations(options).map(i => i.name), + ); + }); + + test('it works with defaultIntegrations=false', () => { + const options = { + defaultIntegrations: false, + } as const; + const actual = applyDefaultOptions(options); + + expect(actual.defaultIntegrations).toStrictEqual(false); + }); + + test('it works with defaultIntegrations=[]', () => { + const options = { + defaultIntegrations: [], + }; + const actual = applyDefaultOptions(options); + + expect(actual.defaultIntegrations).toEqual([]); + }); + + test('it works with tracesSampleRate=undefined', () => { + const options = { + tracesSampleRate: undefined, + } as const; + const actual = applyDefaultOptions(options); + + // Not defined, not even undefined + expect('tracesSampleRate' in actual).toBe(false); + }); + + test('it works with tracesSampleRate=null', () => { + const options = { + tracesSampleRate: null, + } as any; + const actual = applyDefaultOptions(options); + + expect(actual.tracesSampleRate).toStrictEqual(null); + }); + + test('it works with tracesSampleRate=0', () => { + const options = { + tracesSampleRate: 0, + } as const; + const actual = applyDefaultOptions(options); + + expect(actual.tracesSampleRate).toStrictEqual(0); + }); + + test('it does not deep-drop undefined keys', () => { + const options = { + obj: { + prop: undefined, + }, + } as any; + const actual = applyDefaultOptions(options) as any; + + expect('prop' in actual.obj).toBe(true); + expect(actual.obj.prop).toStrictEqual(undefined); + }); +}); diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index 03b57f0438e2..0b659332df99 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -426,7 +426,7 @@ describe('browserTracingIntegration', () => { const pageloadSpan = getActiveSpan(); expect(spanToJSON(pageloadSpan!).description).toBe('changed'); - expect(spanToJSON(pageloadSpan!).data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom'); + expect(spanToJSON(pageloadSpan!).data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom'); }); describe('startBrowserTracingNavigationSpan', () => { @@ -608,7 +608,7 @@ describe('browserTracingIntegration', () => { const pageloadSpan = getActiveSpan(); expect(spanToJSON(pageloadSpan!).description).toBe('changed'); - expect(spanToJSON(pageloadSpan!).data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom'); + expect(spanToJSON(pageloadSpan!).data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom'); }); it('sets the navigation span name on `scope.transactionName`', () => { @@ -643,20 +643,20 @@ describe('browserTracingIntegration', () => { const newCurrentScopePropCtx = getCurrentScope().getPropagationContext(); expect(oldCurrentScopePropCtx).toEqual({ - spanId: expect.stringMatching(/[a-f0-9]{16}/), traceId: expect.stringMatching(/[a-f0-9]{32}/), + sampleRand: expect.any(Number), }); expect(oldIsolationScopePropCtx).toEqual({ - spanId: expect.stringMatching(/[a-f0-9]{16}/), traceId: expect.stringMatching(/[a-f0-9]{32}/), + sampleRand: expect.any(Number), }); expect(newCurrentScopePropCtx).toEqual({ - spanId: expect.stringMatching(/[a-f0-9]{16}/), traceId: expect.stringMatching(/[a-f0-9]{32}/), + sampleRand: expect.any(Number), }); expect(newIsolationScopePropCtx).toEqual({ - spanId: expect.stringMatching(/[a-f0-9]{16}/), traceId: expect.stringMatching(/[a-f0-9]{32}/), + sampleRand: expect.any(Number), }); expect(newIsolationScopePropCtx.traceId).not.toEqual(oldIsolationScopePropCtx.traceId); @@ -680,7 +680,7 @@ describe('browserTracingIntegration', () => { const propCtxBeforeEnd = getCurrentScope().getPropagationContext(); expect(propCtxBeforeEnd).toStrictEqual({ - spanId: expect.stringMatching(/[a-f0-9]{16}/), + sampleRand: expect.any(Number), traceId: expect.stringMatching(/[a-f0-9]{32}/), }); @@ -688,10 +688,9 @@ describe('browserTracingIntegration', () => { const propCtxAfterEnd = getCurrentScope().getPropagationContext(); expect(propCtxAfterEnd).toStrictEqual({ - // eslint-disable-next-line deprecation/deprecation - spanId: propCtxBeforeEnd.spanId, traceId: propCtxBeforeEnd.traceId, sampled: true, + sampleRand: expect.any(Number), dsc: { environment: 'production', public_key: 'examplePublicKey', @@ -699,6 +698,7 @@ describe('browserTracingIntegration', () => { sampled: 'true', transaction: 'mySpan', trace_id: propCtxBeforeEnd.traceId, + sample_rand: expect.any(String), }, }); }); @@ -720,18 +720,17 @@ describe('browserTracingIntegration', () => { const propCtxBeforeEnd = getCurrentScope().getPropagationContext(); expect(propCtxBeforeEnd).toStrictEqual({ - spanId: expect.stringMatching(/[a-f0-9]{16}/), traceId: expect.stringMatching(/[a-f0-9]{32}/), + sampleRand: expect.any(Number), }); navigationSpan!.end(); const propCtxAfterEnd = getCurrentScope().getPropagationContext(); expect(propCtxAfterEnd).toStrictEqual({ - // eslint-disable-next-line deprecation/deprecation - spanId: propCtxBeforeEnd.spanId, traceId: propCtxBeforeEnd.traceId, sampled: false, + sampleRand: expect.any(Number), dsc: { environment: 'production', public_key: 'examplePublicKey', @@ -739,6 +738,7 @@ describe('browserTracingIntegration', () => { sampled: 'false', transaction: 'mySpan', trace_id: propCtxBeforeEnd.traceId, + sample_rand: expect.any(String), }, }); }); @@ -749,7 +749,7 @@ describe('browserTracingIntegration', () => { // make sampled false here, so we can see that it's being used rather than the tracesSampleRate-dictated one document.head.innerHTML = '' + - ''; + ''; const client = new BrowserClient( getDefaultBrowserClientOptions({ @@ -775,11 +775,12 @@ describe('browserTracingIntegration', () => { expect(spanIsSampled(idleSpan)).toBe(false); expect(dynamicSamplingContext).toBeDefined(); - expect(dynamicSamplingContext).toStrictEqual({ release: '2.1.14' }); + expect(dynamicSamplingContext).toStrictEqual({ release: '2.1.14', sample_rand: '0.123' }); // Propagation context keeps the meta tag trace data for later events on the same route to add them to the trace expect(propagationContext.traceId).toEqual('12312012123120121231201212312012'); expect(propagationContext.parentSpanId).toEqual('1121201211212012'); + expect(propagationContext.sampleRand).toBe(0.123); }); it('puts frozen Dynamic Sampling Context on pageload span if sentry-trace data and only 3rd party baggage is present', () => { @@ -859,6 +860,7 @@ describe('browserTracingIntegration', () => { public_key: 'examplePublicKey', sample_rate: '1', sampled: 'true', + sample_rand: expect.any(String), trace_id: expect.not.stringContaining('12312012123120121231201212312012'), }); @@ -871,7 +873,7 @@ describe('browserTracingIntegration', () => { // make sampled false here, so we can see that it's being used rather than the tracesSampleRate-dictated one document.head.innerHTML = '' + - ''; + ''; const client = new BrowserClient( getDefaultBrowserClientOptions({ @@ -891,7 +893,7 @@ describe('browserTracingIntegration', () => { }, { sentryTrace: '12312012123120121231201212312011-1121201211212011-1', - baggage: 'sentry-release=2.2.14,foo=bar', + baggage: 'sentry-release=2.2.14,foo=bar,sentry-sample_rand=0.123', }, ); @@ -908,11 +910,12 @@ describe('browserTracingIntegration', () => { expect(spanIsSampled(idleSpan)).toBe(true); expect(dynamicSamplingContext).toBeDefined(); - expect(dynamicSamplingContext).toStrictEqual({ release: '2.2.14' }); + expect(dynamicSamplingContext).toStrictEqual({ release: '2.2.14', sample_rand: '0.123' }); // Propagation context keeps the custom trace data for later events on the same route to add them to the trace expect(propagationContext.traceId).toEqual('12312012123120121231201212312011'); expect(propagationContext.parentSpanId).toEqual('1121201211212011'); + expect(propagationContext.sampleRand).toEqual(0.123); }); }); diff --git a/packages/browser/test/tracing/request.test.ts b/packages/browser/test/tracing/request.test.ts index 337d08caff24..b262053190eb 100644 --- a/packages/browser/test/tracing/request.test.ts +++ b/packages/browser/test/tracing/request.test.ts @@ -1,9 +1,9 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { MockInstance } from 'vitest'; +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import * as browserUtils from '@sentry-internal/browser-utils'; import * as utils from '@sentry/core'; import type { Client } from '@sentry/core'; -import { WINDOW } from '../../src/helpers'; import { extractNetworkProtocol, instrumentOutgoingRequests, shouldAttachHeaders } from '../../src/tracing/request'; @@ -131,18 +131,14 @@ describe('shouldAttachHeaders', () => { }); describe('with no defined `tracePropagationTargets`', () => { - let originalWindowLocation: Location; - - beforeAll(() => { - originalWindowLocation = WINDOW.location; - // @ts-expect-error Override delete - delete WINDOW.location; - // @ts-expect-error We are missing some fields of the Origin interface but it doesn't matter for these tests. - WINDOW.location = new URL('https://my-origin.com'); + let locationHrefSpy: MockInstance; + + beforeEach(() => { + locationHrefSpy = vi.spyOn(utils, 'getLocationHref').mockImplementation(() => 'https://my-origin.com'); }); - afterAll(() => { - WINDOW.location = originalWindowLocation; + afterEach(() => { + locationHrefSpy.mockReset(); }); it.each([ @@ -173,18 +169,16 @@ describe('shouldAttachHeaders', () => { }); describe('with `tracePropagationTargets`', () => { - let originalWindowLocation: Location; - - beforeAll(() => { - originalWindowLocation = WINDOW.location; - // @ts-expect-error Override delete - delete WINDOW.location; - // @ts-expect-error We are missing some fields of the Origin interface but it doesn't matter for these tests. - WINDOW.location = new URL('https://my-origin.com/api/my-route'); + let locationHrefSpy: MockInstance; + + beforeEach(() => { + locationHrefSpy = vi + .spyOn(utils, 'getLocationHref') + .mockImplementation(() => 'https://my-origin.com/api/my-route'); }); - afterAll(() => { - WINDOW.location = originalWindowLocation; + afterEach(() => { + locationHrefSpy.mockReset(); }); it.each([ @@ -298,18 +292,14 @@ describe('shouldAttachHeaders', () => { }); describe('when window.location.href is not available', () => { - let originalWindowLocation: Location; - - beforeAll(() => { - originalWindowLocation = WINDOW.location; - // @ts-expect-error Override delete - delete WINDOW.location; - // @ts-expect-error We need to simulate an edge-case - WINDOW.location = undefined; + let locationHrefSpy: MockInstance; + + beforeEach(() => { + locationHrefSpy = vi.spyOn(utils, 'getLocationHref').mockImplementation(() => ''); }); - afterAll(() => { - WINDOW.location = originalWindowLocation; + afterEach(() => { + locationHrefSpy.mockReset(); }); describe('with no defined `tracePropagationTargets`', () => { diff --git a/packages/browser/test/transports/offline.test.ts b/packages/browser/test/transports/offline.test.ts index a9a396949588..070d6623f967 100644 --- a/packages/browser/test/transports/offline.test.ts +++ b/packages/browser/test/transports/offline.test.ts @@ -64,6 +64,7 @@ describe('makeOfflineTransport', () => { await deleteDatabase('sentry'); (global as any).TextEncoder = TextEncoder; (global as any).TextDecoder = TextDecoder; + (global as any).addEventListener = () => {}; }); it('indexedDb wrappers push, unshift and pop', async () => { @@ -115,4 +116,32 @@ describe('makeOfflineTransport', () => { expect(queuedCount).toEqual(1); expect(getSendCount()).toEqual(2); }); + + it('flush forces retry', async () => { + const { getSendCount, baseTransport } = createTestTransport(new Error(), { statusCode: 200 }, { statusCode: 200 }); + let queuedCount = 0; + const transport = makeBrowserOfflineTransport(baseTransport)({ + ...transportOptions, + shouldStore: () => { + queuedCount += 1; + return true; + }, + url: 'http://localhost', + }); + const result = await transport.send(ERROR_ENVELOPE); + + expect(result).toEqual({}); + + await delay(MIN_DELAY * 2); + + expect(getSendCount()).toEqual(0); + expect(queuedCount).toEqual(1); + + await transport.flush(); + + await delay(MIN_DELAY * 2); + + expect(queuedCount).toEqual(1); + expect(getSendCount()).toEqual(1); + }); }); diff --git a/packages/bun/.eslintrc.js b/packages/bun/.eslintrc.js index 9d915d4f4c3b..6da218bd8641 100644 --- a/packages/bun/.eslintrc.js +++ b/packages/bun/.eslintrc.js @@ -4,8 +4,6 @@ module.exports = { }, extends: ['../../.eslintrc.js'], rules: { - '@sentry-internal/sdk/no-optional-chaining': 'off', - '@sentry-internal/sdk/no-nullish-coalescing': 'off', '@sentry-internal/sdk/no-class-field-initializers': 'off', }, }; diff --git a/packages/bun/package.json b/packages/bun/package.json index ce1c85cbcd0f..7f72b54d9894 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.18" + "node": ">=18" }, "files": [ "/build" @@ -29,7 +29,7 @@ } }, "typesVersions": { - "<4.9": { + "<5.0": { "build/types/index.d.ts": [ "build/types-ts3.8/index.d.ts" ] diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 1ba5f2de4786..8617030f12d9 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -2,8 +2,6 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, - // eslint-disable-next-line deprecation/deprecation - Request, RequestEventData, SdkInfo, Event, @@ -18,7 +16,6 @@ export type { Thread, User, } from '@sentry/core'; -export type { AddRequestDataToEventOptions } from '@sentry/core'; export { addEventProcessor, @@ -34,8 +31,6 @@ export { endSession, withMonitor, createTransport, - // eslint-disable-next-line deprecation/deprecation - getCurrentHub, getClient, isInitialized, generateInstrumentOnce, @@ -64,11 +59,6 @@ export { flush, close, getSentryRelease, - // eslint-disable-next-line deprecation/deprecation - addRequestDataToEvent, - DEFAULT_USER_INCLUDES, - // eslint-disable-next-line deprecation/deprecation - extractRequestData, createGetModuleFromFilename, anrIntegration, disableAnrDetectionForCallback, @@ -99,8 +89,6 @@ export { continueTrace, getAutoPerformanceIntegrations, cron, - // eslint-disable-next-line deprecation/deprecation - metrics, parameterize, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -127,10 +115,6 @@ export { mysql2Integration, redisIntegration, tediousIntegration, - // eslint-disable-next-line deprecation/deprecation - nestIntegration, - // eslint-disable-next-line deprecation/deprecation - setupNestErrorHandler, postgresIntegration, prismaIntegration, hapiIntegration, @@ -141,8 +125,7 @@ export { spanToTraceHeader, spanToBaggageHeader, trpcMiddleware, - // eslint-disable-next-line deprecation/deprecation - addOpenTelemetryInstrumentation, + updateSpanName, zodErrorsIntegration, profiler, amqplibIntegration, @@ -150,13 +133,9 @@ export { export { captureConsoleIntegration, - // eslint-disable-next-line deprecation/deprecation - debugIntegration, dedupeIntegration, extraErrorDataIntegration, rewriteFramesIntegration, - // eslint-disable-next-line deprecation/deprecation - sessionTimingIntegration, } from '@sentry/core'; export type { BunOptions } from './types'; diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 862d5bd87212..d8ee46abae73 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -100,7 +100,7 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] const response = await (fetchTarget.apply(fetchThisArg, fetchArgs) as ReturnType< typeof serveOptions.fetch >); - if (response && response.status) { + if (response?.status) { setHttpStatus(span, response.status); isolationScope.setContext('response', { headers: response.headers.toJSON(), diff --git a/packages/cloudflare/.eslintrc.js b/packages/cloudflare/.eslintrc.js index 9d915d4f4c3b..6da218bd8641 100644 --- a/packages/cloudflare/.eslintrc.js +++ b/packages/cloudflare/.eslintrc.js @@ -4,8 +4,6 @@ module.exports = { }, extends: ['../../.eslintrc.js'], rules: { - '@sentry-internal/sdk/no-optional-chaining': 'off', - '@sentry-internal/sdk/no-nullish-coalescing': 'off', '@sentry-internal/sdk/no-class-field-initializers': 'off', }, }; diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index efec51c5c0f5..2d114652b6ab 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.18" + "node": ">=18" }, "files": [ "/build" @@ -29,7 +29,7 @@ } }, "typesVersions": { - "<4.9": { + "<5.0": { "build/types/index.d.ts": [ "build/types-ts3.8/index.d.ts" ] @@ -46,7 +46,7 @@ }, "devDependencies": { "@cloudflare/workers-types": "^4.20240725.0", - "@types/node": "^14.18.0", + "@types/node": "^18.19.1", "wrangler": "^3.67.1" }, "scripts": { diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index f3c80b8ddf32..2aedf4362aea 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -2,8 +2,6 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, - // eslint-disable-next-line deprecation/deprecation - Request, RequestEventData, SdkInfo, Event, @@ -18,7 +16,6 @@ export type { Thread, User, } from '@sentry/core'; -export type { AddRequestDataToEventOptions } from '@sentry/core'; export type { CloudflareOptions } from './client'; @@ -67,15 +64,11 @@ export { withActiveSpan, getSpanDescendants, continueTrace, - // eslint-disable-next-line deprecation/deprecation - metrics, functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration, requestDataIntegration, extraErrorDataIntegration, - // eslint-disable-next-line deprecation/deprecation - debugIntegration, dedupeIntegration, rewriteFramesIntegration, captureConsoleIntegration, @@ -89,6 +82,7 @@ export { spanToJSON, spanToTraceHeader, spanToBaggageHeader, + updateSpanName, } from '@sentry/core'; export { withSentry } from './handler'; diff --git a/packages/cloudflare/src/integrations/fetch.ts b/packages/cloudflare/src/integrations/fetch.ts index 651d41f826a1..fa1a85a7b868 100644 --- a/packages/cloudflare/src/integrations/fetch.ts +++ b/packages/cloudflare/src/integrations/fetch.ts @@ -124,8 +124,12 @@ function createBreadcrumb(handlerData: HandlerDataFetch): void { return; } + const breadcrumbData: FetchBreadcrumbData = { + method: handlerData.fetchData.method, + url: handlerData.fetchData.url, + }; + if (handlerData.error) { - const data = handlerData.fetchData; const hint: FetchBreadcrumbHint = { data: handlerData.error, input: handlerData.args, @@ -136,29 +140,31 @@ function createBreadcrumb(handlerData: HandlerDataFetch): void { addBreadcrumb( { category: 'fetch', - data, + data: breadcrumbData, level: 'error', type: 'http', }, hint, ); } else { - const data: FetchBreadcrumbData = { - ...handlerData.fetchData, - status_code: handlerData.response && handlerData.response.status, - }; + const response = handlerData.response as Response | undefined; + + breadcrumbData.request_body_size = handlerData.fetchData.request_body_size; + breadcrumbData.response_body_size = handlerData.fetchData.response_body_size; + breadcrumbData.status_code = response?.status; + const hint: FetchBreadcrumbHint = { input: handlerData.args, - response: handlerData.response, + response, startTimestamp, endTimestamp, }; - const level = getBreadcrumbLogLevelFromHttpStatusCode(data.status_code); + const level = getBreadcrumbLogLevelFromHttpStatusCode(breadcrumbData.status_code); addBreadcrumb( { category: 'fetch', - data, + data: breadcrumbData, type: 'http', level, }, diff --git a/packages/cloudflare/test/integrations/fetch.test.ts b/packages/cloudflare/test/integrations/fetch.test.ts index 43a3c749e64b..b0d301de3519 100644 --- a/packages/cloudflare/test/integrations/fetch.test.ts +++ b/packages/cloudflare/test/integrations/fetch.test.ts @@ -26,7 +26,6 @@ describe('WinterCGFetch instrumentation', () => { client = new FakeClient({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - enableTracing: true, tracesSampleRate: 1, integrations: [], transport: () => ({ @@ -171,7 +170,6 @@ describe('WinterCGFetch instrumentation', () => { method: 'POST', status_code: 201, url: 'http://my-website.com/', - __span: expect.any(String), }, type: 'http', }, diff --git a/packages/core/package.json b/packages/core/package.json index ab43b79117b9..03d947c37bca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -7,7 +7,7 @@ "author": "Sentry", "license": "MIT", "engines": { - "node": ">=14.18" + "node": ">=18" }, "files": [ "/build" @@ -29,7 +29,7 @@ } }, "typesVersions": { - "<4.9": { + "<5.0": { "build/types/index.d.ts": [ "build/types-ts3.8/index.d.ts" ] @@ -38,11 +38,6 @@ "publishConfig": { "access": "public" }, - "TODO(v9):": "Remove these dependencies", - "devDependencies": { - "@types/array.prototype.flat": "^1.2.1", - "array.prototype.flat": "^1.3.0" - }, "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index 23b0306d2e27..0c0e75176c61 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -1,3 +1,4 @@ +import type { ReportDialogOptions } from './report-dialog'; import type { DsnComponents, DsnLike, SdkInfo } from './types-hoist'; import { dsnToString, makeDsn } from './utils-hoist/dsn'; @@ -44,15 +45,7 @@ export function getEnvelopeEndpointWithUrlEncodedAuth(dsn: DsnComponents, tunnel } /** Returns the url to the report dialog endpoint. */ -export function getReportDialogEndpoint( - dsnLike: DsnLike, - dialogOptions: { - // TODO(v9): Change this to [key: string]: unknown; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; - user?: { name?: string; email?: string }; - }, -): string { +export function getReportDialogEndpoint(dsnLike: DsnLike, dialogOptions: ReportDialogOptions): string { const dsn = makeDsn(dsnLike); if (!dsn) { return ''; diff --git a/packages/core/src/asyncContext/stackStrategy.ts b/packages/core/src/asyncContext/stackStrategy.ts index 68c72fb8e92d..cb0bf878b39c 100644 --- a/packages/core/src/asyncContext/stackStrategy.ts +++ b/packages/core/src/asyncContext/stackStrategy.ts @@ -1,13 +1,13 @@ +import type { Client } from '../client'; import { getDefaultCurrentScope, getDefaultIsolationScope } from '../defaultScopes'; import { Scope } from '../scope'; -import type { Client, Scope as ScopeInterface } from '../types-hoist'; import { isThenable } from '../utils-hoist/is'; import { getMainCarrier, getSentryCarrier } from './../carrier'; import type { AsyncContextStrategy } from './types'; interface Layer { client?: Client; - scope: ScopeInterface; + scope: Scope; } /** @@ -15,9 +15,9 @@ interface Layer { */ export class AsyncContextStack { private readonly _stack: [Layer, ...Layer[]]; - private _isolationScope: ScopeInterface; + private _isolationScope: Scope; - public constructor(scope?: ScopeInterface, isolationScope?: ScopeInterface) { + public constructor(scope?: Scope, isolationScope?: Scope) { let assignedScope; if (!scope) { assignedScope = new Scope(); @@ -40,7 +40,7 @@ export class AsyncContextStack { /** * Fork a scope for the stack. */ - public withScope(callback: (scope: ScopeInterface) => T): T { + public withScope(callback: (scope: Scope) => T): T { const scope = this._pushScope(); let maybePromiseResult: T; @@ -79,14 +79,14 @@ export class AsyncContextStack { /** * Returns the scope of the top stack. */ - public getScope(): ScopeInterface { + public getScope(): Scope { return this.getStackTop().scope; } /** * Get the isolation scope for the stack. */ - public getIsolationScope(): ScopeInterface { + public getIsolationScope(): Scope { return this._isolationScope; } @@ -100,7 +100,7 @@ export class AsyncContextStack { /** * Push a scope to the stack. */ - private _pushScope(): ScopeInterface { + private _pushScope(): Scope { // We want to clone the content of prev scope const scope = this.getScope().clone(); this._stack.push({ @@ -130,11 +130,11 @@ function getAsyncContextStack(): AsyncContextStack { return (sentry.stack = sentry.stack || new AsyncContextStack(getDefaultCurrentScope(), getDefaultIsolationScope())); } -function withScope(callback: (scope: ScopeInterface) => T): T { +function withScope(callback: (scope: Scope) => T): T { return getAsyncContextStack().withScope(callback); } -function withSetScope(scope: ScopeInterface, callback: (scope: ScopeInterface) => T): T { +function withSetScope(scope: Scope, callback: (scope: Scope) => T): T { const stack = getAsyncContextStack() as AsyncContextStack; return stack.withScope(() => { stack.getStackTop().scope = scope; @@ -142,7 +142,7 @@ function withSetScope(scope: ScopeInterface, callback: (scope: ScopeInterface }); } -function withIsolationScope(callback: (isolationScope: ScopeInterface) => T): T { +function withIsolationScope(callback: (isolationScope: Scope) => T): T { return getAsyncContextStack().withScope(() => { return callback(getAsyncContextStack().getIsolationScope()); }); @@ -156,7 +156,7 @@ export function getStackAsyncContextStrategy(): AsyncContextStrategy { withIsolationScope, withScope, withSetScope, - withSetIsolationScope: (_isolationScope: ScopeInterface, callback: (isolationScope: ScopeInterface) => T) => { + withSetIsolationScope: (_isolationScope: Scope, callback: (isolationScope: Scope) => T) => { return withIsolationScope(callback); }, getCurrentScope: () => getAsyncContextStack().getScope(), diff --git a/packages/core/src/asyncContext/types.ts b/packages/core/src/asyncContext/types.ts index 7b5bf8acc54c..97af0af1b88a 100644 --- a/packages/core/src/asyncContext/types.ts +++ b/packages/core/src/asyncContext/types.ts @@ -1,6 +1,7 @@ -import type { Scope } from '../types-hoist'; +import type { Scope } from '../scope'; import type { getTraceData } from '../utils/traceData'; import type { + continueTrace, startInactiveSpan, startSpan, startSpanManual, @@ -68,4 +69,11 @@ export interface AsyncContextStrategy { /** Get trace data as serialized string values for propagation via `sentry-trace` and `baggage`. */ getTraceData?: typeof getTraceData; + + /** + * 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. + */ + continueTrace?: typeof continueTrace; } diff --git a/packages/core/src/carrier.ts b/packages/core/src/carrier.ts index d053234b4929..5136121cb8ae 100644 --- a/packages/core/src/carrier.ts +++ b/packages/core/src/carrier.ts @@ -1,6 +1,7 @@ import type { AsyncContextStack } from './asyncContext/stackStrategy'; import type { AsyncContextStrategy } from './asyncContext/types'; -import type { Client, Integration, MetricsAggregator, Scope } from './types-hoist'; +import type { Scope } from './scope'; +import type { Logger } from './utils-hoist/logger'; import { SDK_VERSION } from './utils-hoist/version'; import { GLOBAL_OBJ } from './utils-hoist/worldwide'; @@ -16,21 +17,19 @@ type VersionedCarrier = { version?: string; } & Record, SentryCarrier>; -interface SentryCarrier { +export interface SentryCarrier { acs?: AsyncContextStrategy; stack?: AsyncContextStack; globalScope?: Scope; defaultIsolationScope?: Scope; defaultCurrentScope?: Scope; - globalMetricsAggregators?: WeakMap | undefined; - - // TODO(v9): Remove these properties - they are no longer used and were left over in v8 - integrations?: Integration[]; - extensions?: { - // eslint-disable-next-line @typescript-eslint/ban-types - [key: string]: Function; - }; + logger?: Logger; + + /** Overwrites TextEncoder used in `@sentry/core`, need for `react-native@0.73` and older */ + encodePolyfill?: (input: string) => Uint8Array; + /** Overwrites TextDecoder used in `@sentry/core`, need for `react-native@0.73` and older */ + decodePolyfill?: (input: Uint8Array) => string; } /** @@ -57,3 +56,25 @@ export function getSentryCarrier(carrier: Carrier): SentryCarrier { // rather than what's set in .version so that "this" SDK always gets its carrier return (__SENTRY__[SDK_VERSION] = __SENTRY__[SDK_VERSION] || {}); } + +/** + * Returns a global singleton contained in the global `__SENTRY__[]` object. + * + * If the singleton doesn't already exist in `__SENTRY__`, it will be created using the given factory + * function and added to the `__SENTRY__` object. + * + * @param name name of the global singleton on __SENTRY__ + * @param creator creator Factory function to create the singleton if it doesn't already exist on `__SENTRY__` + * @param obj (Optional) The global object on which to look for `__SENTRY__`, if not `GLOBAL_OBJ`'s return value + * @returns the singleton + */ +export function getGlobalSingleton( + name: Prop, + creator: () => NonNullable, + obj = GLOBAL_OBJ, +): NonNullable { + const __SENTRY__ = (obj.__SENTRY__ = obj.__SENTRY__ || {}); + const carrier = (__SENTRY__[SDK_VERSION] = __SENTRY__[SDK_VERSION] || {}); + // Note: We do not want to set `carrier.version` here, as this may be called before any `init` is called, e.g. for the default scopes + return carrier[name] || (carrier[name] = creator()); +} diff --git a/packages/core/src/checkin.ts b/packages/core/src/checkin.ts index 34f2428cbcfb..44b460376916 100644 --- a/packages/core/src/checkin.ts +++ b/packages/core/src/checkin.ts @@ -24,7 +24,7 @@ export function createCheckInEnvelope( sent_at: new Date().toISOString(), }; - if (metadata && metadata.sdk) { + if (metadata?.sdk) { headers.sdk = { name: metadata.sdk.name, version: metadata.sdk.version, diff --git a/packages/core/src/baseclient.ts b/packages/core/src/client.ts similarity index 65% rename from packages/core/src/baseclient.ts rename to packages/core/src/client.ts index c394a0d77a95..7334b2d294ed 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/client.ts @@ -2,7 +2,7 @@ import type { Breadcrumb, BreadcrumbHint, - Client, + CheckIn, ClientOptions, DataCategory, DsnComponents, @@ -15,6 +15,7 @@ import type { EventProcessor, FeedbackEvent, Integration, + MonitorConfig, Outcome, ParameterizedString, SdkMetadata, @@ -32,6 +33,7 @@ import type { } from './types-hoist'; import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; +import { DEFAULT_ENVIRONMENT } from './constants'; import { getCurrentScope, getIsolationScope, getTraceContextFromScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; @@ -46,14 +48,18 @@ import { dsnToString, makeDsn } from './utils-hoist/dsn'; import { addItemToEnvelope, createAttachmentEnvelopeItem } from './utils-hoist/envelope'; import { SentryError } from './utils-hoist/error'; import { isParameterizedString, isPlainObject, isPrimitive, isThenable } from './utils-hoist/is'; -import { consoleSandbox, logger } from './utils-hoist/logger'; +import { logger } from './utils-hoist/logger'; import { checkOrSetAlreadyCaught, uuid4 } from './utils-hoist/misc'; import { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils-hoist/syncpromise'; +import { getPossibleEventMessages } from './utils/eventUtils'; +import { merge } from './utils/merge'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; import { showSpanDropWarning } from './utils/spanUtils'; +import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; +const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing or non-string release'; /** * Base implementation for all JavaScript SDK clients. @@ -68,7 +74,7 @@ const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been ca * without a valid Dsn, the SDK will not send any events to Sentry. * * Before sending an event, it is passed through - * {@link BaseClient._prepareEvent} to add SDK information and scope data + * {@link Client._prepareEvent} to add SDK information and scope data * (breadcrumbs and context). To add more custom information, override this * method and extend the resulting prepared event. * @@ -78,7 +84,7 @@ const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been ca * {@link Client.addBreadcrumb}. * * @example - * class NodeClient extends BaseClient { + * class NodeClient extends Client { * public constructor(options: NodeOptions) { * super(options); * } @@ -86,7 +92,7 @@ const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been ca * // ... * } */ -export abstract class BaseClient implements Client { +export abstract class Client { /** Options passed to the SDK. */ protected readonly _options: O; @@ -141,22 +147,12 @@ export abstract class BaseClient implements Client { url, }); } - - // TODO(v9): Remove this deprecation warning - const tracingOptions = ['enableTracing', 'tracesSampleRate', 'tracesSampler'] as const; - const undefinedOption = tracingOptions.find(option => option in options && options[option] == undefined); - if (undefinedOption) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - `[Sentry] Deprecation warning: \`${undefinedOption}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`, - ); - }); - } } /** - * @inheritDoc + * Captures an exception event and sends it to Sentry. + * + * Unlike `captureException` exported from every SDK, this method requires that you pass it the current scope. */ public captureException(exception: unknown, hint?: EventHint, scope?: Scope): string { const eventId = uuid4(); @@ -182,7 +178,9 @@ export abstract class BaseClient implements Client { } /** - * @inheritDoc + * Captures a message event and sends it to Sentry. + * + * Unlike `captureMessage` exported from every SDK, this method requires that you pass it the current scope. */ public captureMessage( message: ParameterizedString, @@ -207,13 +205,15 @@ export abstract class BaseClient implements Client { } /** - * @inheritDoc + * Captures a manually created event and sends it to Sentry. + * + * Unlike `captureEvent` exported from every SDK, this method requires that you pass it the current scope. */ public captureEvent(event: Event, hint?: EventHint, currentScope?: Scope): string { const eventId = uuid4(); // ensure we haven't captured this very object before - if (hint && hint.originalException && checkOrSetAlreadyCaught(hint.originalException)) { + if (hint?.originalException && checkOrSetAlreadyCaught(hint.originalException)) { DEBUG_BUILD && logger.log(ALREADY_SEEN_ERROR); return eventId; } @@ -225,57 +225,72 @@ export abstract class BaseClient implements Client { const sdkProcessingMetadata = event.sdkProcessingMetadata || {}; const capturedSpanScope: Scope | undefined = sdkProcessingMetadata.capturedSpanScope; + const capturedSpanIsolationScope: Scope | undefined = sdkProcessingMetadata.capturedSpanIsolationScope; - this._process(this._captureEvent(event, hintWithEventId, capturedSpanScope || currentScope)); + this._process( + this._captureEvent(event, hintWithEventId, capturedSpanScope || currentScope, capturedSpanIsolationScope), + ); return hintWithEventId.event_id; } /** - * @inheritDoc + * Captures a session. */ public captureSession(session: Session): void { - if (!(typeof session.release === 'string')) { - DEBUG_BUILD && logger.warn('Discarded session because of missing or non-string release'); - } else { - this.sendSession(session); - // After sending, we set init false to indicate it's not the first occurrence - updateSession(session, { init: false }); - } + this.sendSession(session); + // After sending, we set init false to indicate it's not the first occurrence + updateSession(session, { init: false }); } /** - * @inheritDoc + * Create a cron monitor check in and send it to Sentry. This method is not available on all clients. + * + * @param checkIn An object that describes a check in. + * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want + * to create a monitor automatically when sending a check in. + * @param scope An optional scope containing event metadata. + * @returns A string representing the id of the check in. + */ + public captureCheckIn?(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string; + + /** + * Get the current Dsn. */ public getDsn(): DsnComponents | undefined { return this._dsn; } /** - * @inheritDoc + * Get the current options. */ public getOptions(): O { return this._options; } /** + * Get the SDK metadata. * @see SdkMetadata - * - * @return The metadata of the SDK */ public getSdkMetadata(): SdkMetadata | undefined { return this._options._metadata; } /** - * @inheritDoc + * Returns the transport that is used by the client. + * Please note that the transport gets lazy initialized so it will only be there once the first event has been sent. */ public getTransport(): Transport | undefined { return this._transport; } /** - * @inheritDoc + * Wait for all events to be sent or the timeout to expire, whichever comes first. + * + * @param timeout Maximum time in ms the client should wait for events to be flushed. Omitting this parameter will + * cause the client to wait until all events are sent before resolving the promise. + * @returns A promise that will resolve with `true` if all events are sent before the timeout, or `false` if there are + * still events in the queue when the timeout is reached. */ public flush(timeout?: number): PromiseLike { const transport = this._transport; @@ -290,7 +305,12 @@ export abstract class BaseClient implements Client { } /** - * @inheritDoc + * Flush the event queue and set the client to `enabled = false`. See {@link Client.flush}. + * + * @param {number} timeout Maximum time in ms the client should wait before shutting down. Omitting this parameter will cause + * the client to wait until all events are sent before disabling itself. + * @returns {Promise} A promise which resolves to `true` if the flush completes successfully before the timeout, or `false` if + * it doesn't. */ public close(timeout?: number): PromiseLike { return this.flush(timeout).then(result => { @@ -300,17 +320,24 @@ export abstract class BaseClient implements Client { }); } - /** Get all installed event processors. */ + /** + * Get all installed event processors. + */ public getEventProcessors(): EventProcessor[] { return this._eventProcessors; } - /** @inheritDoc */ + /** + * Adds an event processor that applies to any event processed by this client. + */ public addEventProcessor(eventProcessor: EventProcessor): void { this._eventProcessors.push(eventProcessor); } - /** @inheritdoc */ + /** + * Initialize this client. + * Call this after the client was set on a scope. + */ public init(): void { if ( this._isEnabled() || @@ -328,14 +355,18 @@ export abstract class BaseClient implements Client { /** * Gets an installed integration by its name. * - * @returns The installed integration or `undefined` if no integration with that `name` was installed. + * @returns {Integration|undefined} The installed integration or `undefined` if no integration with that `name` was installed. */ public getIntegrationByName(integrationName: string): T | undefined { return this._integrations[integrationName] as T | undefined; } /** - * @inheritDoc + * Add an integration to the client. + * This can be used to e.g. lazy load integrations. + * In most cases, this should not be necessary, + * and you're better off just passing the integrations via `integrations: []` at initialization time. + * However, if you find the need to conditionally load & add an integration, you can use `addIntegration` to do so. */ public addIntegration(integration: Integration): void { const isAlreadyInstalled = this._integrations[integration.name]; @@ -349,7 +380,7 @@ export abstract class BaseClient implements Client { } /** - * @inheritDoc + * Send a fully prepared event to Sentry. */ public sendEvent(event: Event, hint: EventHint = {}): void { this.emit('beforeSendEvent', event, hint); @@ -367,9 +398,31 @@ export abstract class BaseClient implements Client { } /** - * @inheritDoc + * Send a session or session aggregrates to Sentry. */ public sendSession(session: Session | SessionAggregates): void { + // Backfill release and environment on session + const { release: clientReleaseOption, environment: clientEnvironmentOption = DEFAULT_ENVIRONMENT } = this._options; + if ('aggregates' in session) { + const sessionAttrs = session.attrs || {}; + if (!sessionAttrs.release && !clientReleaseOption) { + DEBUG_BUILD && logger.warn(MISSING_RELEASE_FOR_SESSION_ERROR); + return; + } + sessionAttrs.release = sessionAttrs.release || clientReleaseOption; + sessionAttrs.environment = sessionAttrs.environment || clientEnvironmentOption; + session.attrs = sessionAttrs; + } else { + if (!session.release && !clientReleaseOption) { + DEBUG_BUILD && logger.warn(MISSING_RELEASE_FOR_SESSION_ERROR); + return; + } + session.release = session.release || clientReleaseOption; + session.environment = session.environment || clientEnvironmentOption; + } + + this.emit('beforeSendSession', session); + const env = createSessionEnvelope(session, this._dsn, this._options._metadata, this._options.tunnel); // sendEnvelope should not throw @@ -378,14 +431,10 @@ export abstract class BaseClient implements Client { } /** - * @inheritDoc + * Record on the client that an event got dropped (ie, an event that will not be sent to Sentry). */ - public recordDroppedEvent(reason: EventDropReason, category: DataCategory, eventOrCount?: Event | number): void { + public recordDroppedEvent(reason: EventDropReason, category: DataCategory, count: number = 1): void { if (this._options.sendClientReports) { - // TODO v9: We do not need the `event` passed as third argument anymore, and can possibly remove this overload - // If event is passed as third argument, we assume this is a count of 1 - const count = typeof eventOrCount === 'number' ? eventOrCount : 1; - // We want to track each category (error, transaction, session, replay_event) separately // but still keep the distinction between different type of outcomes. // We could use nested maps, but it's much easier to read and type this way. @@ -398,60 +447,124 @@ export abstract class BaseClient implements Client { } } - // Keep on() & emit() signatures in sync with types' client.ts interface /* eslint-disable @typescript-eslint/unified-signatures */ - - /** @inheritdoc */ + /** + * Register a callback for whenever a span is started. + * Receives the span as argument. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on(hook: 'spanStart', callback: (span: Span) => void): () => void; - /** @inheritdoc */ + /** + * Register a callback before span sampling runs. Receives a `samplingDecision` object argument with a `decision` + * property that can be used to make a sampling decision that will be enforced, before any span sampling runs. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on( + hook: 'beforeSampling', + callback: ( + samplingData: { + spanAttributes: SpanAttributes; + spanName: string; + parentSampled?: boolean; + parentContext?: SpanContextData; + }, + samplingDecision: { decision: boolean }, + ) => void, + ): void; + + /** + * Register a callback for whenever a span is ended. + * Receives the span as argument. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on(hook: 'spanEnd', callback: (span: Span) => void): () => void; - /** @inheritdoc */ + /** + * Register a callback for when an idle span is allowed to auto-finish. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): () => void; - /** @inheritdoc */ + /** + * Register a callback for transaction start and finish. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): () => void; - /** @inheritdoc */ - public on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint) => void): () => void; + /** + * Register a callback that runs when stack frame metadata should be applied to an event. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'applyFrameMetadata', callback: (event: Event) => void): () => void; - /** @inheritdoc */ - public on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint) => void): () => void; + /** + * Register a callback for before sending an event. + * This is called right before an event is sent and should not be used to mutate the event. + * Receives an Event & EventHint as arguments. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint | undefined) => void): () => void; - /** @inheritdoc */ + /** + * Register a callback for before sending a session or session aggregrates.. + * Receives the session/aggregate as second argument. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'beforeSendSession', callback: (session: Session | SessionAggregates) => void): () => void; + + /** + * Register a callback for preprocessing an event, + * before it is passed to (global) event processors. + * Receives an Event & EventHint as arguments. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint | undefined) => void): () => void; + + /** + * Register a callback for postprocessing an event, + * after it was passed to (global) event processors, before it is being sent. + * Receives an Event & EventHint as arguments. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'postprocessEvent', callback: (event: Event, hint?: EventHint | undefined) => void): () => void; + + /** + * Register a callback for when an event has been sent. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on( hook: 'afterSendEvent', callback: (event: Event, sendResponse: TransportMakeRequestResponse) => void, ): () => void; - /** @inheritdoc */ + /** + * Register a callback before a breadcrumb is added. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): () => void; - /** @inheritdoc */ + /** + * Register a callback when a DSC (Dynamic Sampling Context) is created. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext, rootSpan?: Span) => void): () => void; - /** @inheritdoc */ + /** + * Register a callback when a Feedback event has been prepared. + * This should be used to mutate the event. The options argument can hint + * about what kind of mutation it expects. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on( hook: 'beforeSendFeedback', - callback: (feedback: FeedbackEvent, options?: { includeReplay: boolean }) => void, + callback: (feedback: FeedbackEvent, options?: { includeReplay?: boolean }) => void, ): () => void; - /** @inheritdoc */ - public on( - hook: 'beforeSampling', - callback: ( - samplingData: { - spanAttributes: SpanAttributes; - spanName: string; - parentSampled?: boolean; - parentContext?: SpanContextData; - }, - samplingDecision: { decision: boolean }, - ) => void, - ): void; - - /** @inheritdoc */ + /** + * A hook for the browser tracing integrations to trigger a span start for a page load. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on( hook: 'startPageLoadSpan', callback: ( @@ -460,16 +573,27 @@ export abstract class BaseClient implements Client { ) => void, ): () => void; - /** @inheritdoc */ + /** + * A hook for browser tracing integrations to trigger a span for a navigation. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): () => void; + /** + * A hook that is called when the client is flushing + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on(hook: 'flush', callback: () => void): () => void; + /** + * A hook that is called when the client is closing + * @returns {() => void} A function that, when executed, removes the registered callback. + */ public on(hook: 'close', callback: () => void): () => void; - public on(hook: 'applyFrameMetadata', callback: (event: Event) => void): () => void; - - /** @inheritdoc */ + /** + * Register a hook oin this client. + */ public on(hook: string, callback: unknown): () => void { const hooks = (this._hooks[hook] = this._hooks[hook] || []); @@ -489,7 +613,10 @@ export abstract class BaseClient implements Client { }; } - /** @inheritdoc */ + /** Fire a hook whenever a span starts. */ + public emit(hook: 'spanStart', span: Span): void; + + /** A hook that is called every time before a span is sampled. */ public emit( hook: 'beforeSampling', samplingData: { @@ -501,56 +628,100 @@ export abstract class BaseClient implements Client { samplingDecision: { decision: boolean }, ): void; - /** @inheritdoc */ - public emit(hook: 'spanStart', span: Span): void; - - /** @inheritdoc */ + /** Fire a hook whenever a span ends. */ public emit(hook: 'spanEnd', span: Span): void; - /** @inheritdoc */ + /** + * Fire a hook indicating that an idle span is allowed to auto finish. + */ public emit(hook: 'idleSpanEnableAutoFinish', span: Span): void; - /** @inheritdoc */ + /* + * Fire a hook event for envelope creation and sending. Expects to be given an envelope as the + * second argument. + */ public emit(hook: 'beforeEnvelope', envelope: Envelope): void; - /** @inheritdoc */ + /* + * Fire a hook indicating that stack frame metadata should be applied to the event passed to the hook. + */ + public emit(hook: 'applyFrameMetadata', event: Event): void; + + /** + * Fire a hook event before sending an event. + * This is called right before an event is sent and should not be used to mutate the event. + * Expects to be given an Event & EventHint as the second/third argument. + */ public emit(hook: 'beforeSendEvent', event: Event, hint?: EventHint): void; - /** @inheritdoc */ + /** + * Fire a hook event before sending a session/aggregates. + * Expects to be given the prepared session/aggregates as second argument. + */ + public emit(hook: 'beforeSendSession', session: Session | SessionAggregates): void; + + /** + * Fire a hook event to process events before they are passed to (global) event processors. + * Expects to be given an Event & EventHint as the second/third argument. + */ public emit(hook: 'preprocessEvent', event: Event, hint?: EventHint): void; - /** @inheritdoc */ + /** + * Fire a hook event to process a user on an event before it is sent to Sentry, after all other processors have run. + * Expects to be given an Event & EventHint as the second/third argument. + */ + public emit(hook: 'postprocessEvent', event: Event, hint?: EventHint): void; + + /* + * Fire a hook event after sending an event. Expects to be given an Event as the + * second argument. + */ public emit(hook: 'afterSendEvent', event: Event, sendResponse: TransportMakeRequestResponse): void; - /** @inheritdoc */ + /** + * Fire a hook for when a breadcrumb is added. Expects the breadcrumb as second argument. + */ public emit(hook: 'beforeAddBreadcrumb', breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void; - /** @inheritdoc */ + /** + * Fire a hook for when a DSC (Dynamic Sampling Context) is created. Expects the DSC as second argument. + */ public emit(hook: 'createDsc', dsc: DynamicSamplingContext, rootSpan?: Span): void; - /** @inheritdoc */ - public emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay: boolean }): void; + /** + * Fire a hook event for after preparing a feedback event. Events to be given + * a feedback event as the second argument, and an optional options object as + * third argument. + */ + public emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay?: boolean }): void; - /** @inheritdoc */ + /** + * Emit a hook event for browser tracing integrations to trigger a span start for a page load. + */ public emit( hook: 'startPageLoadSpan', options: StartSpanOptions, traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined }, ): void; - /** @inheritdoc */ + /** + * Emit a hook event for browser tracing integrations to trigger a span for a navigation. + */ public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void; - /** @inheritdoc */ + /** + * Emit a hook event for client flush + */ public emit(hook: 'flush'): void; - /** @inheritdoc */ + /** + * Emit a hook event for client close + */ public emit(hook: 'close'): void; - /** @inheritdoc */ - public emit(hook: 'applyFrameMetadata', event: Event): void; - - /** @inheritdoc */ + /** + * Emit a hook that was previously registered via `on()`. + */ public emit(hook: string, ...rest: unknown[]): void { const callbacks = this._hooks[hook]; if (callbacks) { @@ -559,7 +730,7 @@ export abstract class BaseClient implements Client { } /** - * @inheritdoc + * Send an envelope to Sentry. */ public sendEnvelope(envelope: Envelope): PromiseLike { this.emit('beforeEnvelope', envelope); @@ -587,16 +758,16 @@ export abstract class BaseClient implements Client { /** Updates existing session based on the provided event */ protected _updateSessionFromEvent(session: Session, event: Event): void { - let crashed = false; + let crashed = event.level === 'fatal'; let errored = false; - const exceptions = event.exception && event.exception.values; + const exceptions = event.exception?.values; if (exceptions) { errored = true; for (const ex of exceptions) { const mechanism = ex.mechanism; - if (mechanism && mechanism.handled === false) { + if (mechanism?.handled === false) { crashed = true; break; } @@ -670,12 +841,12 @@ export abstract class BaseClient implements Client { protected _prepareEvent( event: Event, hint: EventHint, - currentScope = getCurrentScope(), - isolationScope = getIsolationScope(), + currentScope: Scope, + isolationScope: Scope, ): PromiseLike { const options = this.getOptions(); const integrations = Object.keys(this._integrations); - if (!hint.integrations && integrations.length > 0) { + if (!hint.integrations && integrations?.length) { hint.integrations = integrations; } @@ -690,6 +861,8 @@ export abstract class BaseClient implements Client { return evt; } + this.emit('postprocessEvent', evt, hint); + evt.contexts = { trace: getTraceContextFromScope(currentScope), ...evt.contexts, @@ -712,8 +885,17 @@ export abstract class BaseClient implements Client { * @param hint * @param scope */ - protected _captureEvent(event: Event, hint: EventHint = {}, scope?: Scope): PromiseLike { - return this._processEvent(event, hint, scope).then( + protected _captureEvent( + event: Event, + hint: EventHint = {}, + currentScope = getCurrentScope(), + isolationScope = getIsolationScope(), + ): PromiseLike { + if (DEBUG_BUILD && isErrorEvent(event)) { + logger.log(`Captured error event \`${getPossibleEventMessages(event)[0] || ''}\``); + } + + return this._processEvent(event, hint, currentScope, isolationScope).then( finalEvent => { return finalEvent.event_id; }, @@ -746,7 +928,12 @@ export abstract class BaseClient implements Client { * @param currentScope A scope containing event metadata. * @returns A SyncPromise that resolves with the event or rejects in case event was/will not be send. */ - protected _processEvent(event: Event, hint: EventHint, currentScope?: Scope): PromiseLike { + protected _processEvent( + event: Event, + hint: EventHint, + currentScope: Scope, + isolationScope: Scope, + ): PromiseLike { const options = this.getOptions(); const { sampleRate } = options; @@ -760,7 +947,7 @@ export abstract class BaseClient implements Client { // Sampling for transaction happens somewhere else const parsedSampleRate = typeof sampleRate === 'undefined' ? undefined : parseSampleRate(sampleRate); if (isError && typeof parsedSampleRate === 'number' && Math.random() > parsedSampleRate) { - this.recordDroppedEvent('sample_rate', 'error', event); + this.recordDroppedEvent('sample_rate', 'error'); return rejectedSyncPromise( new SentryError( `Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`, @@ -769,15 +956,12 @@ export abstract class BaseClient implements Client { ); } - const dataCategory: DataCategory = eventType === 'replay_event' ? 'replay' : eventType; + const dataCategory = (eventType === 'replay_event' ? 'replay' : eventType) satisfies DataCategory; - const sdkProcessingMetadata = event.sdkProcessingMetadata || {}; - const capturedSpanIsolationScope: Scope | undefined = sdkProcessingMetadata.capturedSpanIsolationScope; - - return this._prepareEvent(event, hint, currentScope, capturedSpanIsolationScope) + return this._prepareEvent(event, hint, currentScope, isolationScope) .then(prepared => { if (prepared === null) { - this.recordDroppedEvent('event_processor', dataCategory, event); + this.recordDroppedEvent('event_processor', dataCategory); throw new SentryError('An event processor returned `null`, will not send event.', 'log'); } @@ -791,7 +975,7 @@ export abstract class BaseClient implements Client { }) .then(processedEvent => { if (processedEvent === null) { - this.recordDroppedEvent('before_send', dataCategory, event); + this.recordDroppedEvent('before_send', dataCategory); if (isTransaction) { const spans = event.spans || []; // the transaction itself counts as one span, plus all the child spans that are added @@ -801,15 +985,13 @@ export abstract class BaseClient implements Client { throw new SentryError(`${beforeSendLabel} returned \`null\`, will not send event.`, 'log'); } - const session = currentScope && currentScope.getSession(); - if (!isTransaction && session) { + const session = currentScope.getSession() || isolationScope.getSession(); + if (isError && session) { this._updateSessionFromEvent(session, processedEvent); } if (isTransaction) { - const spanCountBefore = - (processedEvent.sdkProcessingMetadata && processedEvent.sdkProcessingMetadata.spanCountBeforeProcessing) || - 0; + const spanCountBefore = processedEvent.sdkProcessingMetadata?.spanCountBeforeProcessing || 0; const spanCountAfter = processedEvent.spans ? processedEvent.spans.length : 0; const droppedSpanCount = spanCountBefore - spanCountAfter; @@ -912,12 +1094,12 @@ export abstract class BaseClient implements Client { } /** - * @inheritDoc + * Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`. */ public abstract eventFromException(_exception: unknown, _hint?: EventHint): PromiseLike; /** - * @inheritDoc + * Creates an {@link Event} from primitive inputs to `captureMessage`. */ public abstract eventFromMessage( _message: ParameterizedString, @@ -926,6 +1108,18 @@ export abstract class BaseClient implements Client { ): PromiseLike; } +/** + * @deprecated Use `Client` instead. This alias may be removed in a future major version. + */ +// TODO(v10): Remove +export type BaseClient = Client; + +/** + * @deprecated Use `Client` instead. This alias may be removed in a future major version. + */ +// TODO(v10): Remove +export const BaseClient = Client; + /** * Verifies that return value of configured `beforeSend` or `beforeSendTransaction` is of expected type, and returns the value if so. */ @@ -962,41 +1156,54 @@ function processBeforeSend( hint: EventHint, ): PromiseLike | Event | null { const { beforeSend, beforeSendTransaction, beforeSendSpan } = options; + let processedEvent = event; - if (isErrorEvent(event) && beforeSend) { - return beforeSend(event, hint); + if (isErrorEvent(processedEvent) && beforeSend) { + return beforeSend(processedEvent, hint); } - if (isTransactionEvent(event)) { - if (event.spans && beforeSendSpan) { - const processedSpans: SpanJSON[] = []; - for (const span of event.spans) { - const processedSpan = beforeSendSpan(span); - if (processedSpan) { - processedSpans.push(processedSpan); - } else { - showSpanDropWarning(); - client.recordDroppedEvent('before_send', 'span'); + if (isTransactionEvent(processedEvent)) { + if (beforeSendSpan) { + // process root span + const processedRootSpanJson = beforeSendSpan(convertTransactionEventToSpanJson(processedEvent)); + if (!processedRootSpanJson) { + showSpanDropWarning(); + } else { + // update event with processed root span values + processedEvent = merge(event, convertSpanJsonToTransactionEvent(processedRootSpanJson)); + } + + // process child spans + if (processedEvent.spans) { + const processedSpans: SpanJSON[] = []; + for (const span of processedEvent.spans) { + const processedSpan = beforeSendSpan(span); + if (!processedSpan) { + showSpanDropWarning(); + processedSpans.push(span); + } else { + processedSpans.push(processedSpan); + } } + processedEvent.spans = processedSpans; } - event.spans = processedSpans; } if (beforeSendTransaction) { - if (event.spans) { + if (processedEvent.spans) { // We store the # of spans before processing in SDK metadata, // so we can compare it afterwards to determine how many spans were dropped - const spanCountBefore = event.spans.length; - event.sdkProcessingMetadata = { + const spanCountBefore = processedEvent.spans.length; + processedEvent.sdkProcessingMetadata = { ...event.sdkProcessingMetadata, spanCountBeforeProcessing: spanCountBefore, }; } - return beforeSendTransaction(event, hint); + return beforeSendTransaction(processedEvent as TransactionEvent, hint); } } - return event; + return processedEvent; } function isErrorEvent(event: Event): event is ErrorEvent { diff --git a/packages/core/src/currentScopes.ts b/packages/core/src/currentScopes.ts index 85b148738467..6fab81298530 100644 --- a/packages/core/src/currentScopes.ts +++ b/packages/core/src/currentScopes.ts @@ -1,9 +1,10 @@ import { getAsyncContextStrategy } from './asyncContext'; -import { getMainCarrier } from './carrier'; -import { Scope as ScopeClass } from './scope'; -import type { Client, Scope, TraceContext } from './types-hoist'; +import { getGlobalSingleton, getMainCarrier } from './carrier'; +import type { Client } from './client'; +import { Scope } from './scope'; +import type { TraceContext } from './types-hoist'; +import { generateSpanId } from './utils-hoist'; import { dropUndefinedKeys } from './utils-hoist/object'; -import { getGlobalSingleton } from './utils-hoist/worldwide'; /** * Get the currently active scope. @@ -29,7 +30,7 @@ export function getIsolationScope(): Scope { * This scope is applied to _all_ events. */ export function getGlobalScope(): Scope { - return getGlobalSingleton('globalScope', () => new ScopeClass()); + return getGlobalSingleton('globalScope', () => new Scope()); } /** @@ -127,13 +128,11 @@ export function getClient(): C | undefined { export function getTraceContextFromScope(scope: Scope): TraceContext { const propagationContext = scope.getPropagationContext(); - // TODO(v9): Use generateSpanId() instead of spanId - // eslint-disable-next-line deprecation/deprecation - const { traceId, spanId, parentSpanId } = propagationContext; + const { traceId, parentSpanId, propagationSpanId } = propagationContext; const traceContext: TraceContext = dropUndefinedKeys({ trace_id: traceId, - span_id: spanId, + span_id: propagationSpanId || generateSpanId(), parent_span_id: parentSpanId, }); diff --git a/packages/core/src/defaultScopes.ts b/packages/core/src/defaultScopes.ts index c9fb32c2049e..cf8701e57e37 100644 --- a/packages/core/src/defaultScopes.ts +++ b/packages/core/src/defaultScopes.ts @@ -1,13 +1,12 @@ -import { Scope as ScopeClass } from './scope'; -import type { Scope } from './types-hoist'; -import { getGlobalSingleton } from './utils-hoist/worldwide'; +import { getGlobalSingleton } from './carrier'; +import { Scope } from './scope'; /** Get the default current scope. */ export function getDefaultCurrentScope(): Scope { - return getGlobalSingleton('defaultCurrentScope', () => new ScopeClass()); + return getGlobalSingleton('defaultCurrentScope', () => new Scope()); } /** Get the default isolation scope. */ export function getDefaultIsolationScope(): Scope { - return getGlobalSingleton('defaultIsolationScope', () => new ScopeClass()); + return getGlobalSingleton('defaultIsolationScope', () => new Scope()); } diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 999fb0681cf0..89e231c305fc 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -1,7 +1,7 @@ +import type { Client } from './client'; import { getDynamicSamplingContextFromSpan } from './tracing/dynamicSamplingContext'; import type { SentrySpan } from './tracing/sentrySpan'; import type { - Client, DsnComponents, DynamicSamplingContext, Event, @@ -18,7 +18,6 @@ import type { SessionItem, SpanEnvelope, SpanItem, - SpanJSON, } from './types-hoist'; import { dsnToString } from './utils-hoist/dsn'; import { @@ -86,7 +85,7 @@ export function createEventEnvelope( */ const eventType = event.type && event.type !== 'replay_event' ? event.type : 'event'; - enhanceEventWithSdkInfo(event, metadata && metadata.sdk); + enhanceEventWithSdkInfo(event, metadata?.sdk); const envelopeHeaders = createEventEnvelopeHeaders(event, sdkInfo, tunnel, dsn); @@ -115,8 +114,8 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? // different segments in one envelope const dsc = getDynamicSamplingContextFromSpan(spans[0]); - const dsn = client && client.getDsn(); - const tunnel = client && client.getOptions().tunnel; + const dsn = client?.getDsn(); + const tunnel = client?.getOptions().tunnel; const headers: SpanEnvelope[0] = { sent_at: new Date().toISOString(), @@ -124,16 +123,20 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }), }; - const beforeSendSpan = client && client.getOptions().beforeSendSpan; + const beforeSendSpan = client?.getOptions().beforeSendSpan; const convertToSpanJSON = beforeSendSpan ? (span: SentrySpan) => { - const spanJson = beforeSendSpan(spanToJSON(span) as SpanJSON); - if (!spanJson) { + const spanJson = spanToJSON(span); + const processedSpan = beforeSendSpan(spanJson); + + if (!processedSpan) { showSpanDropWarning(); + return spanJson; } - return spanJson; + + return processedSpan; } - : (span: SentrySpan) => spanToJSON(span); + : spanToJSON; const items: SpanItem[] = []; for (const span of spans) { diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index cf7e872fb001..4854ee86efb8 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -1,5 +1,8 @@ +import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from './currentScopes'; +import { DEBUG_BUILD } from './debug-build'; +import type { CaptureContext } from './scope'; +import { closeSession, makeSession, updateSession } from './session'; import type { - CaptureContext, CheckIn, Event, EventHint, @@ -14,11 +17,6 @@ import type { SeverityLevel, User, } from './types-hoist'; - -import { DEFAULT_ENVIRONMENT } from './constants'; -import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from './currentScopes'; -import { DEBUG_BUILD } from './debug-build'; -import { closeSession, makeSession, updateSession } from './session'; import { isThenable } from './utils-hoist/is'; import { logger } from './utils-hoist/logger'; import { uuid4 } from './utils-hoist/misc'; @@ -245,7 +243,7 @@ export function isInitialized(): boolean { /** If the SDK is initialized & enabled. */ export function isEnabled(): boolean { const client = getClient(); - return !!client && client.getOptions().enabled !== false && !!client.getTransport(); + return client?.getOptions().enabled !== false && !!client?.getTransport(); } /** @@ -265,18 +263,13 @@ export function addEventProcessor(callback: EventProcessor): void { * @returns the new active session */ export function startSession(context?: SessionContext): Session { - const client = getClient(); const isolationScope = getIsolationScope(); const currentScope = getCurrentScope(); - const { release, environment = DEFAULT_ENVIRONMENT } = (client && client.getOptions()) || {}; - // Will fetch userAgent if called from browser sdk const { userAgent } = GLOBAL_OBJ.navigator || {}; const session = makeSession({ - release, - environment, user: currentScope.getUser() || isolationScope.getUser(), ...(userAgent && { userAgent }), ...context, @@ -284,7 +277,7 @@ export function startSession(context?: SessionContext): Session { // End existing session if there's one const currentSession = isolationScope.getSession(); - if (currentSession && currentSession.status === 'ok') { + if (currentSession?.status === 'ok') { updateSession(currentSession, { status: 'exited' }); } @@ -293,10 +286,6 @@ export function startSession(context?: SessionContext): Session { // Afterwards we set the new session on the scope isolationScope.setSession(session); - // TODO (v8): Remove this and only use the isolation scope(?). - // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() - currentScope.setSession(session); - return session; } @@ -315,10 +304,6 @@ export function endSession(): void { // the session is over; take it off of the scope isolationScope.setSession(); - - // TODO (v8): Remove this and only use the isolation scope(?). - // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() - currentScope.setSession(); } /** @@ -326,11 +311,8 @@ export function endSession(): void { */ function _sendSessionUpdate(): void { const isolationScope = getIsolationScope(); - const currentScope = getCurrentScope(); const client = getClient(); - // TODO (v8): Remove currentScope and only use the isolation scope(?). - // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() - const session = currentScope.getSession() || isolationScope.getSession(); + const session = isolationScope.getSession(); if (session && client) { client.captureSession(session); } diff --git a/packages/core/src/feedback.ts b/packages/core/src/feedback.ts index 95a5bc4fa2a9..088248102012 100644 --- a/packages/core/src/feedback.ts +++ b/packages/core/src/feedback.ts @@ -28,7 +28,7 @@ export function captureFeedback( tags, }; - const client = (scope && scope.getClient()) || getClient(); + const client = scope?.getClient() || getClient(); if (client) { client.emit('beforeSendFeedback', feedbackEvent, hint); diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 55ea867a763a..8998eb45fce0 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -1,7 +1,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes'; import { SPAN_STATUS_ERROR, setHttpStatus, startInactiveSpan } from './tracing'; import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; -import type { Client, HandlerDataFetch, Scope, Span, SpanOrigin } from './types-hoist'; +import type { HandlerDataFetch, Span, SpanOrigin } from './types-hoist'; import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils-hoist/baggage'; import { isInstanceOf } from './utils-hoist/is'; import { parseUrl } from './utils-hoist/url'; @@ -199,27 +199,6 @@ function _addTracingHeadersToFetchRequest( } } -/** - * Adds sentry-trace and baggage headers to the various forms of fetch headers. - * - * @deprecated This function will not be exported anymore in v9. - */ -export function addTracingHeadersToFetchRequest( - request: string | unknown, - _client: Client | undefined, - _scope: Scope | undefined, - fetchOptionsObj: { - headers?: - | { - [key: string]: string[] | string | undefined; - } - | PolymorphicRequestHeaders; - }, - span?: Span, -): PolymorphicRequestHeaders | undefined { - return _addTracingHeadersToFetchRequest(request as Request, fetchOptionsObj, span); -} - function getFullURL(url: string): string | undefined { try { const parsed = new URL(url); @@ -233,8 +212,7 @@ function endSpan(span: Span, handlerData: HandlerDataFetch): void { if (handlerData.response) { setHttpStatus(span, handlerData.response.status); - const contentLength = - handlerData.response && handlerData.response.headers && handlerData.response.headers.get('content-length'); + const contentLength = handlerData.response?.headers && handlerData.response.headers.get('content-length'); if (contentLength) { const contentLengthNum = parseInt(contentLength); diff --git a/packages/core/src/getCurrentHubShim.ts b/packages/core/src/getCurrentHubShim.ts deleted file mode 100644 index ceea470a727c..000000000000 --- a/packages/core/src/getCurrentHubShim.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { addBreadcrumb } from './breadcrumbs'; -import { getClient, getCurrentScope, getIsolationScope, withScope } from './currentScopes'; -import { - captureEvent, - endSession, - setContext, - setExtra, - setExtras, - setTag, - setTags, - setUser, - startSession, -} from './exports'; -import type { Client, EventHint, Hub, Integration, IntegrationClass, SeverityLevel } from './types-hoist'; - -/** - * This is for legacy reasons, and returns a proxy object instead of a hub to be used. - * - * @deprecated Use the methods directly from the top level Sentry API (e.g. `Sentry.withScope`) - * For more information see our migration guide for - * [replacing `getCurrentHub` and `Hub`](https://github.com/getsentry/sentry-javascript/blob/develop/MIGRATION.md#deprecate-hub) - * usage - */ -// eslint-disable-next-line deprecation/deprecation -export function getCurrentHubShim(): Hub { - return { - bindClient(client: Client): void { - const scope = getCurrentScope(); - scope.setClient(client); - }, - - withScope, - getClient: () => getClient() as C | undefined, - getScope: getCurrentScope, - getIsolationScope, - captureException: (exception: unknown, hint?: EventHint) => { - return getCurrentScope().captureException(exception, hint); - }, - captureMessage: (message: string, level?: SeverityLevel, hint?: EventHint) => { - return getCurrentScope().captureMessage(message, level, hint); - }, - captureEvent, - addBreadcrumb, - setUser, - setTags, - setTag, - setExtra, - setExtras, - setContext, - - getIntegration(integration: IntegrationClass): T | null { - const client = getClient(); - return (client && client.getIntegrationByName(integration.id)) || null; - }, - - startSession, - endSession, - captureSession(end?: boolean): void { - // both send the update and pull the session from the scope - if (end) { - return endSession(); - } - - // only send the update - _sendSessionUpdate(); - }, - }; -} - -/** - * Returns the default hub instance. - * - * If a hub is already registered in the global carrier but this module - * contains a more recent version, it replaces the registered version. - * Otherwise, the currently registered hub will be returned. - * - * @deprecated Use the respective replacement method directly instead. - */ -// eslint-disable-next-line deprecation/deprecation -export const getCurrentHub = getCurrentHubShim; - -/** - * Sends the current Session on the scope - */ -function _sendSessionUpdate(): void { - const scope = getCurrentScope(); - const client = getClient(); - - const session = scope.getSession(); - if (client && session) { - client.captureSession(session); - } -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 77259d2434d4..2c89d0e8a60b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,7 +3,6 @@ export type { AsyncContextStrategy } from './asyncContext/types'; export type { Carrier } from './carrier'; export type { OfflineStore, OfflineTransportOptions } from './transports/offline'; export type { ServerRuntimeClientOptions } from './server-runtime-client'; -export type { RequestDataIntegrationOptions } from './integrations/requestdata'; export type { IntegrationIndex } from './integration'; export * from './tracing'; @@ -45,14 +44,17 @@ export { getDefaultIsolationScope, } from './defaultScopes'; export { setAsyncContextStrategy } from './asyncContext'; -export { getMainCarrier } from './carrier'; +export { getGlobalSingleton, getMainCarrier } from './carrier'; export { makeSession, closeSession, updateSession } from './session'; -// eslint-disable-next-line deprecation/deprecation -export { SessionFlusher } from './sessionflusher'; export { Scope } from './scope'; +export type { CaptureContext, ScopeContext, ScopeData } from './scope'; export { notifyEventProcessors } from './eventProcessors'; export { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint } from './api'; -export { BaseClient } from './baseclient'; +export { + Client, + // eslint-disable-next-line deprecation/deprecation + BaseClient, +} from './client'; export { ServerRuntimeClient } from './server-runtime-client'; export { initAndBind, setCurrentClient } from './sdk'; export { createTransport } from './transports/base'; @@ -81,11 +83,19 @@ export { getActiveSpan, addChildSpanToSpan, spanTimeInputToSeconds, + updateSpanName, } from './utils/spanUtils'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; export { getTraceData } from './utils/traceData'; export { getTraceMetaTags } from './utils/meta'; +export { + winterCGHeadersToDict, + winterCGRequestToRequestData, + httpRequestToRequestData, + extractQueryParamsFromUrl, + headersToDict, +} from './utils/request'; export { DEFAULT_ENVIRONMENT } from './constants'; export { addBreadcrumb } from './breadcrumbs'; export { functionToStringIntegration } from './integrations/functiontostring'; @@ -94,36 +104,20 @@ export { linkedErrorsIntegration } from './integrations/linkederrors'; export { moduleMetadataIntegration } from './integrations/metadata'; export { requestDataIntegration } from './integrations/requestdata'; export { captureConsoleIntegration } from './integrations/captureconsole'; -// eslint-disable-next-line deprecation/deprecation -export { debugIntegration } from './integrations/debug'; export { dedupeIntegration } from './integrations/dedupe'; export { extraErrorDataIntegration } from './integrations/extraerrordata'; export { rewriteFramesIntegration } from './integrations/rewriteframes'; -// eslint-disable-next-line deprecation/deprecation -export { sessionTimingIntegration } from './integrations/sessiontiming'; export { zodErrorsIntegration } from './integrations/zoderrors'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; -// eslint-disable-next-line deprecation/deprecation -export { metrics } from './metrics/exports'; export { profiler } from './profiling'; -// eslint-disable-next-line deprecation/deprecation -export { metricsDefault } from './metrics/exports-default'; -export { BrowserMetricsAggregator } from './metrics/browser-aggregator'; -export { getMetricSummaryJsonForSpan } from './metrics/metric-summary'; -export { - // eslint-disable-next-line deprecation/deprecation - addTracingHeadersToFetchRequest, - instrumentFetchRequest, -} from './fetch'; +export { instrumentFetchRequest } from './fetch'; export { trpcMiddleware } from './trpc'; export { captureFeedback } from './feedback'; +export type { ReportDialogOptions } from './report-dialog'; -// eslint-disable-next-line deprecation/deprecation -export { getCurrentHubShim, getCurrentHub } from './getCurrentHubShim'; - -// TODO(v9): Make this structure pretty again and don't do "export *" +// TODO: Make this structure pretty again and don't do "export *" export * from './utils-hoist/index'; -// TODO(v9): Make this structure pretty again and don't do "export *" +// TODO: Make this structure pretty again and don't do "export *" export * from './types-hoist/index'; export type { FeatureFlag } from './featureFlags'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index f4432ebf2d0a..1d3dcc713934 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,7 +1,7 @@ +import type { Client } from './client'; import { getClient } from './currentScopes'; -import type { Client, Event, EventHint, Integration, IntegrationFn, Options } from './types-hoist'; - import { DEBUG_BUILD } from './debug-build'; +import type { Event, EventHint, Integration, IntegrationFn, Options } from './types-hoist'; import { logger } from './utils-hoist/logger'; export const installedIntegrations: string[] = []; @@ -84,7 +84,7 @@ export function getIntegrationsToSetup(options: Pick { + integrations.forEach((integration: Integration | undefined) => { // guard against empty provided integrations if (integration) { setupIntegration(client, integration, integrationIndex); @@ -100,7 +100,7 @@ export function setupIntegrations(client: Client, integrations: Integration[]): export function afterSetupIntegrations(client: Client, integrations: Integration[]): void { for (const integration of integrations) { // guard against empty provided integrations - if (integration && integration.afterAllSetup) { + if (integration?.afterAllSetup) { integration.afterAllSetup(client); } } diff --git a/packages/core/src/integrations/captureconsole.ts b/packages/core/src/integrations/captureconsole.ts index c180dcbe99e7..203b54180b25 100644 --- a/packages/core/src/integrations/captureconsole.ts +++ b/packages/core/src/integrations/captureconsole.ts @@ -1,7 +1,8 @@ import { getClient, withScope } from '../currentScopes'; import { captureException, captureMessage } from '../exports'; import { defineIntegration } from '../integration'; -import type { CaptureContext, IntegrationFn } from '../types-hoist'; +import type { CaptureContext } from '../scope'; +import type { IntegrationFn } from '../types-hoist'; import { addConsoleInstrumentationHandler } from '../utils-hoist/instrument/console'; import { CONSOLE_LEVELS } from '../utils-hoist/logger'; import { addExceptionMechanism } from '../utils-hoist/misc'; @@ -12,13 +13,11 @@ import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; interface CaptureConsoleOptions { levels?: string[]; - // TODO(v9): Flip default value to `true` and adjust JSDoc! /** - * By default, Sentry will mark captured console messages as unhandled. - * Set this to `true` if you want to mark them as handled instead. + * By default, Sentry will mark captured console messages as handled. + * Set this to `false` if you want to mark them as unhandled instead. * - * Note: in v9 of the SDK, this option will default to `true`, meaning the default behavior will change to mark console messages as handled. - * @default false + * @default true */ handled?: boolean; } @@ -27,8 +26,7 @@ const INTEGRATION_NAME = 'CaptureConsole'; const _captureConsoleIntegration = ((options: CaptureConsoleOptions = {}) => { const levels = options.levels || CONSOLE_LEVELS; - // TODO(v9): Flip default value to `true` - const handled = !!options.handled; + const handled = options.handled ?? true; return { name: INTEGRATION_NAME, diff --git a/packages/core/src/integrations/debug.ts b/packages/core/src/integrations/debug.ts deleted file mode 100644 index 66c70571365a..000000000000 --- a/packages/core/src/integrations/debug.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { defineIntegration } from '../integration'; -import type { Event, EventHint, IntegrationFn } from '../types-hoist'; -import { consoleSandbox } from '../utils-hoist/logger'; - -const INTEGRATION_NAME = 'Debug'; - -interface DebugOptions { - /** Controls whether console output created by this integration should be stringified. Default: `false` */ - stringify?: boolean; - /** Controls whether a debugger should be launched before an event is sent. Default: `false` */ - debugger?: boolean; -} - -const _debugIntegration = ((options: DebugOptions = {}) => { - const _options = { - debugger: false, - stringify: false, - ...options, - }; - - return { - name: INTEGRATION_NAME, - setup(client) { - client.on('beforeSendEvent', (event: Event, hint?: EventHint) => { - if (_options.debugger) { - // eslint-disable-next-line no-debugger - debugger; - } - - /* eslint-disable no-console */ - consoleSandbox(() => { - if (_options.stringify) { - console.log(JSON.stringify(event, null, 2)); - if (hint && Object.keys(hint).length) { - console.log(JSON.stringify(hint, null, 2)); - } - } else { - console.log(event); - if (hint && Object.keys(hint).length) { - console.log(hint); - } - } - }); - /* eslint-enable no-console */ - }); - }, - }; -}) satisfies IntegrationFn; - -/** - * Integration to debug sent Sentry events. - * This integration should not be used in production. - * - * @deprecated This integration is deprecated and will be removed in the next major version of the SDK. - * To log outgoing events, use [Hook Options](https://docs.sentry.io/platforms/javascript/configuration/options/#hooks) (`beforeSend`, `beforeSendTransaction`, ...). - */ -export const debugIntegration = defineIntegration(_debugIntegration); diff --git a/packages/core/src/integrations/dedupe.ts b/packages/core/src/integrations/dedupe.ts index 7687c1f5b73e..5f6b249319aa 100644 --- a/packages/core/src/integrations/dedupe.ts +++ b/packages/core/src/integrations/dedupe.ts @@ -174,5 +174,5 @@ function _isSameFingerprint(currentEvent: Event, previousEvent: Event): boolean } function _getExceptionFromEvent(event: Event): Exception | undefined { - return event.exception && event.exception.values && event.exception.values[0]; + return event.exception?.values && event.exception.values[0]; } diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index cba170bb5722..9ce4a778c326 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -1,6 +1,7 @@ +import type { Client } from '../client'; import { getClient } from '../currentScopes'; import { defineIntegration } from '../integration'; -import type { Client, IntegrationFn, WrappedFunction } from '../types-hoist'; +import type { IntegrationFn, WrappedFunction } from '../types-hoist'; import { getOriginalFunction } from '../utils-hoist/object'; let originalFunctionToString: () => void; diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index 9d9f803a69f5..17b4442cee57 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -5,6 +5,7 @@ import { defineIntegration } from '../integration'; import { logger } from '../utils-hoist/logger'; import { getEventDescription } from '../utils-hoist/misc'; import { stringMatchesSomePattern } from '../utils-hoist/string'; +import { getPossibleEventMessages } from '../utils/eventUtils'; // "Script error." is hard coded into browsers for errors that it can't read. // this is the result of a script being pulled in from an external domain and CORS. @@ -117,7 +118,7 @@ function _isIgnoredError(event: Event, ignoreErrors?: Array): b return false; } - return _getPossibleEventMessages(event).some(message => stringMatchesSomePattern(message, ignoreErrors)); + return getPossibleEventMessages(event).some(message => stringMatchesSomePattern(message, ignoreErrors)); } function _isIgnoredTransaction(event: Event, ignoreTransactions?: Array): boolean { @@ -130,8 +131,7 @@ function _isIgnoredTransaction(event: Event, ignoreTransactions?: Array): boolean { - // TODO: Use Glob instead? - if (!denyUrls || !denyUrls.length) { + if (!denyUrls?.length) { return false; } const url = _getEventFilterUrl(event); @@ -139,41 +139,13 @@ function _isDeniedUrl(event: Event, denyUrls?: Array): boolean } function _isAllowedUrl(event: Event, allowUrls?: Array): boolean { - // TODO: Use Glob instead? - if (!allowUrls || !allowUrls.length) { + if (!allowUrls?.length) { return true; } const url = _getEventFilterUrl(event); return !url ? true : stringMatchesSomePattern(url, allowUrls); } -function _getPossibleEventMessages(event: Event): string[] { - const possibleMessages: string[] = []; - - if (event.message) { - possibleMessages.push(event.message); - } - - let lastException; - try { - // @ts-expect-error Try catching to save bundle size - lastException = event.exception.values[event.exception.values.length - 1]; - } catch (e) { - // try catching to save bundle size checking existence of variables - } - - if (lastException) { - if (lastException.value) { - possibleMessages.push(lastException.value); - if (lastException.type) { - possibleMessages.push(`${lastException.type}: ${lastException.value}`); - } - } - } - - return possibleMessages; -} - function _isSentryError(event: Event): boolean { try { // @ts-expect-error can't be a sentry error if undefined @@ -219,7 +191,7 @@ function _isUselessError(event: Event): boolean { } // We only want to consider events for dropping that actually have recorded exception values. - if (!event.exception || !event.exception.values || event.exception.values.length === 0) { + if (!event.exception?.values?.length) { return false; } diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index 8f23912c5b58..cc80bfe3b4b2 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -1,105 +1,56 @@ import { defineIntegration } from '../integration'; -import type { IntegrationFn } from '../types-hoist'; -import { - type AddRequestDataToEventOptions, - addNormalizedRequestDataToEvent, - addRequestDataToEvent, -} from '../utils-hoist/requestdata'; - -export type RequestDataIntegrationOptions = { - /** - * Controls what data is pulled from the request and added to the event - */ - include?: { - cookies?: boolean; - data?: boolean; - headers?: boolean; - ip?: boolean; - query_string?: boolean; - url?: boolean; - user?: - | boolean - | { - id?: boolean; - username?: boolean; - email?: boolean; - }; - }; +import type { Event, IntegrationFn, RequestEventData } from '../types-hoist'; +import { parseCookie } from '../utils/cookie'; +import { getClientIPAddress, ipHeaderNames } from '../vendor/getIpAddress'; + +interface RequestDataIncludeOptions { + cookies?: boolean; + data?: boolean; + headers?: boolean; + ip?: boolean; + query_string?: boolean; + url?: boolean; +} +type RequestDataIntegrationOptions = { /** - * Whether to identify transactions by parameterized path, parameterized path with method, or handler name. - * @deprecated This option does not do anything anymore, and will be removed in v9. + * Controls what data is pulled from the request and added to the event. */ - transactionNamingScheme?: 'path' | 'methodPath' | 'handler'; + include?: RequestDataIncludeOptions; }; -const DEFAULT_OPTIONS = { - include: { - cookies: true, - data: true, - headers: true, - ip: false, - query_string: true, - url: true, - user: { - id: true, - username: true, - email: true, - }, - }, - transactionNamingScheme: 'methodPath' as const, +const DEFAULT_INCLUDE: RequestDataIncludeOptions = { + cookies: true, + data: true, + headers: true, + query_string: true, + url: true, }; const INTEGRATION_NAME = 'RequestData'; const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) => { - const _options: Required = { - ...DEFAULT_OPTIONS, - ...options, - include: { - ...DEFAULT_OPTIONS.include, - ...options.include, - user: - options.include && typeof options.include.user === 'boolean' - ? options.include.user - : { - ...DEFAULT_OPTIONS.include.user, - // Unclear why TS still thinks `options.include.user` could be a boolean at this point - ...((options.include || {}).user as Record), - }, - }, + const include = { + ...DEFAULT_INCLUDE, + ...options.include, }; return { name: INTEGRATION_NAME, - processEvent(event) { - // Note: In the long run, most of the logic here should probably move into the request data utility functions. For - // the moment it lives here, though, until https://github.com/getsentry/sentry-javascript/issues/5718 is addressed. - // (TL;DR: Those functions touch many parts of the repo in many different ways, and need to be cleaned up. Once - // that's happened, it will be easier to add this logic in without worrying about unexpected side effects.) - + processEvent(event, _hint, client) { const { sdkProcessingMetadata = {} } = event; - const { request, normalizedRequest } = sdkProcessingMetadata; + const { normalizedRequest, ipAddress } = sdkProcessingMetadata; - const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options); + const includeWithDefaultPiiApplied: RequestDataIncludeOptions = { + ...include, + ip: include.ip ?? client.getOptions().sendDefaultPii, + }; - // If this is set, it takes precedence over the plain request object if (normalizedRequest) { - // Some other data is not available in standard HTTP requests, but can sometimes be augmented by e.g. Express or Next.js - const ipAddress = request ? request.ip || (request.socket && request.socket.remoteAddress) : undefined; - const user = request ? request.user : undefined; - - addNormalizedRequestDataToEvent(event, normalizedRequest, { ipAddress, user }, addRequestDataOptions); - return event; + addNormalizedRequestDataToEvent(event, normalizedRequest, { ipAddress }, includeWithDefaultPiiApplied); } - // TODO(v9): Eventually we can remove this fallback branch and only rely on the normalizedRequest above - if (!request) { - return event; - } - - // eslint-disable-next-line deprecation/deprecation - return addRequestDataToEvent(event, request, addRequestDataOptions); + return event; }, }; }) satisfies IntegrationFn; @@ -110,45 +61,75 @@ const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) = */ export const requestDataIntegration = defineIntegration(_requestDataIntegration); -/** Convert this integration's options to match what `addRequestDataToEvent` expects */ -/** TODO: Can possibly be deleted once https://github.com/getsentry/sentry-javascript/issues/5718 is fixed */ -function convertReqDataIntegrationOptsToAddReqDataOpts( - integrationOptions: Required, -): AddRequestDataToEventOptions { - const { - // eslint-disable-next-line deprecation/deprecation - transactionNamingScheme, - include: { ip, user, ...requestOptions }, - } = integrationOptions; - - const requestIncludeKeys: string[] = ['method']; - for (const [key, value] of Object.entries(requestOptions)) { - if (value) { - requestIncludeKeys.push(key); +/** + * Add already normalized request data to an event. + * This mutates the passed in event. + */ +function addNormalizedRequestDataToEvent( + event: Event, + req: RequestEventData, + // Data that should not go into `event.request` but is somehow related to requests + additionalData: { ipAddress?: string }, + include: RequestDataIncludeOptions, +): void { + event.request = { + ...event.request, + ...extractNormalizedRequestData(req, include), + }; + + if (include.ip) { + const ip = (req.headers && getClientIPAddress(req.headers)) || additionalData.ipAddress; + if (ip) { + event.user = { + ...event.user, + ip_address: ip, + }; } } +} - let addReqDataUserOpt; - if (user === undefined) { - addReqDataUserOpt = true; - } else if (typeof user === 'boolean') { - addReqDataUserOpt = user; - } else { - const userIncludeKeys: string[] = []; - for (const [key, value] of Object.entries(user)) { - if (value) { - userIncludeKeys.push(key); - } +function extractNormalizedRequestData( + normalizedRequest: RequestEventData, + include: RequestDataIncludeOptions, +): RequestEventData { + const requestData: RequestEventData = {}; + const headers = { ...normalizedRequest.headers }; + + if (include.headers) { + requestData.headers = headers; + + // Remove the Cookie header in case cookie data should not be included in the event + if (!include.cookies) { + delete (headers as { cookie?: string }).cookie; + } + + // Remove IP headers in case IP data should not be included in the event + if (!include.ip) { + ipHeaderNames.forEach(ipHeaderName => { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (headers as Record)[ipHeaderName]; + }); } - addReqDataUserOpt = userIncludeKeys; } - return { - include: { - ip, - user: addReqDataUserOpt, - request: requestIncludeKeys.length !== 0 ? requestIncludeKeys : undefined, - transaction: transactionNamingScheme, - }, - }; + requestData.method = normalizedRequest.method; + + if (include.url) { + requestData.url = normalizedRequest.url; + } + + if (include.cookies) { + const cookies = normalizedRequest.cookies || (headers?.cookie ? parseCookie(headers.cookie) : undefined); + requestData.cookies = cookies || {}; + } + + if (include.query_string) { + requestData.query_string = normalizedRequest.query_string; + } + + if (include.data) { + requestData.data = normalizedRequest.data; + } + + return requestData; } diff --git a/packages/core/src/integrations/rewriteframes.ts b/packages/core/src/integrations/rewriteframes.ts index ab9d1b812987..3c4ab5aee464 100644 --- a/packages/core/src/integrations/rewriteframes.ts +++ b/packages/core/src/integrations/rewriteframes.ts @@ -54,7 +54,7 @@ export const rewriteFramesIntegration = defineIntegration((options: RewriteFrame const root = options.root; const prefix = options.prefix || 'app:///'; - const isBrowser = 'window' in GLOBAL_OBJ && GLOBAL_OBJ.window !== undefined; + const isBrowser = 'window' in GLOBAL_OBJ && !!GLOBAL_OBJ.window; const iteratee: StackFrameIteratee = options.iteratee || generateIteratee({ isBrowser, root, prefix }); @@ -82,7 +82,7 @@ export const rewriteFramesIntegration = defineIntegration((options: RewriteFrame function _processStacktrace(stacktrace?: Stacktrace): Stacktrace { return { ...stacktrace, - frames: stacktrace && stacktrace.frames && stacktrace.frames.map(f => iteratee(f)), + frames: stacktrace?.frames && stacktrace.frames.map(f => iteratee(f)), }; } diff --git a/packages/core/src/integrations/sessiontiming.ts b/packages/core/src/integrations/sessiontiming.ts deleted file mode 100644 index a7112c4f939c..000000000000 --- a/packages/core/src/integrations/sessiontiming.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { defineIntegration } from '../integration'; -import type { IntegrationFn } from '../types-hoist'; -import { timestampInSeconds } from '../utils-hoist/time'; - -const INTEGRATION_NAME = 'SessionTiming'; - -const _sessionTimingIntegration = (() => { - const startTime = timestampInSeconds() * 1000; - - return { - name: INTEGRATION_NAME, - processEvent(event) { - const now = timestampInSeconds() * 1000; - - return { - ...event, - extra: { - ...event.extra, - ['session:start']: startTime, - ['session:duration']: now - startTime, - ['session:end']: now, - }, - }; - }, - }; -}) satisfies IntegrationFn; - -/** - * This function adds duration since the sessionTimingIntegration was initialized - * till the time event was sent. - * - * @deprecated This integration is deprecated and will be removed in the next major version of the SDK. - * To capture session durations alongside events, use [Context](https://docs.sentry.io/platforms/javascript/enriching-events/context/) (`Sentry.setContext()`). - */ -export const sessionTimingIntegration = defineIntegration(_sessionTimingIntegration); diff --git a/packages/core/src/integrations/zoderrors.ts b/packages/core/src/integrations/zoderrors.ts index fc36925eb0ea..a408285800d9 100644 --- a/packages/core/src/integrations/zoderrors.ts +++ b/packages/core/src/integrations/zoderrors.ts @@ -63,7 +63,7 @@ function formatIssueTitle(issue: ZodIssue): SingleLevelZodIssue { function formatIssueMessage(zodError: ZodError): string { const errorKeyMap = new Set(); for (const iss of zodError.issues) { - if (iss.path && iss.path[0]) { + if (iss.path?.[0]) { errorKeyMap.add(iss.path[0]); } } @@ -77,10 +77,8 @@ function formatIssueMessage(zodError: ZodError): string { */ export function applyZodErrorsToEvent(limit: number, event: Event, hint?: EventHint): Event { if ( - !event.exception || - !event.exception.values || - !hint || - !hint.originalException || + !event.exception?.values || + !hint?.originalException || !originalExceptionIsZodError(hint.originalException) || hint.originalException.issues.length === 0 ) { diff --git a/packages/core/src/metrics/aggregator.ts b/packages/core/src/metrics/aggregator.ts deleted file mode 100644 index 972c6b3336ad..000000000000 --- a/packages/core/src/metrics/aggregator.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { Client, MeasurementUnit, MetricsAggregator as MetricsAggregatorBase, Primitive } from '../types-hoist'; -import { timestampInSeconds } from '../utils-hoist/time'; -import { updateMetricSummaryOnActiveSpan } from '../utils/spanUtils'; -import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, SET_METRIC_TYPE } from './constants'; -import { captureAggregateMetrics } from './envelope'; -import { METRIC_MAP } from './instance'; -import type { MetricBucket, MetricType } from './types'; -import { getBucketKey, sanitizeMetricKey, sanitizeTags, sanitizeUnit } from './utils'; - -/** - * A metrics aggregator that aggregates metrics in memory and flushes them periodically. - */ -export class MetricsAggregator implements MetricsAggregatorBase { - // TODO(@anonrig): Use FinalizationRegistry to have a proper way of flushing the buckets - // when the aggregator is garbage collected. - // Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry - private _buckets: MetricBucket; - - // Different metrics have different weights. We use this to limit the number of metrics - // that we store in memory. - private _bucketsTotalWeight; - - // We adjust the type here to add the `unref()` part, as setInterval can technically return a number or a NodeJS.Timer - private readonly _interval: ReturnType & { unref?: () => void }; - - // SDKs are required to shift the flush interval by random() * rollup_in_seconds. - // That shift is determined once per startup to create jittering. - private readonly _flushShift: number; - - // An SDK is required to perform force flushing ahead of scheduled time if the memory - // pressure is too high. There is no rule for this other than that SDKs should be tracking - // abstract aggregation complexity (eg: a counter only carries a single float, whereas a - // distribution is a float per emission). - // - // Force flush is used on either shutdown, flush() or when we exceed the max weight. - private _forceFlush: boolean; - - public constructor(private readonly _client: Client) { - this._buckets = new Map(); - this._bucketsTotalWeight = 0; - - this._interval = setInterval(() => this._flush(), DEFAULT_FLUSH_INTERVAL); - if (this._interval.unref) { - this._interval.unref(); - } - - this._flushShift = Math.floor((Math.random() * DEFAULT_FLUSH_INTERVAL) / 1000); - this._forceFlush = false; - } - - /** - * @inheritDoc - */ - public add( - metricType: MetricType, - unsanitizedName: string, - value: number | string, - unsanitizedUnit: MeasurementUnit = 'none', - unsanitizedTags: Record = {}, - maybeFloatTimestamp = timestampInSeconds(), - ): void { - const timestamp = Math.floor(maybeFloatTimestamp); - const name = sanitizeMetricKey(unsanitizedName); - const tags = sanitizeTags(unsanitizedTags); - const unit = sanitizeUnit(unsanitizedUnit as string); - - const bucketKey = getBucketKey(metricType, name, unit, tags); - - let bucketItem = this._buckets.get(bucketKey); - // If this is a set metric, we need to calculate the delta from the previous weight. - const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0; - - if (bucketItem) { - bucketItem.metric.add(value); - // TODO(abhi): Do we need this check? - if (bucketItem.timestamp < timestamp) { - bucketItem.timestamp = timestamp; - } - } else { - bucketItem = { - // @ts-expect-error we don't need to narrow down the type of value here, saves bundle size. - metric: new METRIC_MAP[metricType](value), - timestamp, - metricType, - name, - unit, - tags, - }; - this._buckets.set(bucketKey, bucketItem); - } - - // If value is a string, it's a set metric so calculate the delta from the previous weight. - const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value; - updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey); - - // We need to keep track of the total weight of the buckets so that we can - // flush them when we exceed the max weight. - this._bucketsTotalWeight += bucketItem.metric.weight; - - if (this._bucketsTotalWeight >= MAX_WEIGHT) { - this.flush(); - } - } - - /** - * Flushes the current metrics to the transport via the transport. - */ - public flush(): void { - this._forceFlush = true; - this._flush(); - } - - /** - * Shuts down metrics aggregator and clears all metrics. - */ - public close(): void { - this._forceFlush = true; - clearInterval(this._interval); - this._flush(); - } - - /** - * Flushes the buckets according to the internal state of the aggregator. - * If it is a force flush, which happens on shutdown, it will flush all buckets. - * Otherwise, it will only flush buckets that are older than the flush interval, - * and according to the flush shift. - * - * This function mutates `_forceFlush` and `_bucketsTotalWeight` properties. - */ - private _flush(): void { - // TODO(@anonrig): Add Atomics for locking to avoid having force flush and regular flush - // running at the same time. - // Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics - - // This path eliminates the need for checking for timestamps since we're forcing a flush. - // Remember to reset the flag, or it will always flush all metrics. - if (this._forceFlush) { - this._forceFlush = false; - this._bucketsTotalWeight = 0; - this._captureMetrics(this._buckets); - this._buckets.clear(); - return; - } - const cutoffSeconds = Math.floor(timestampInSeconds()) - DEFAULT_FLUSH_INTERVAL / 1000 - this._flushShift; - // TODO(@anonrig): Optimization opportunity. - // Convert this map to an array and store key in the bucketItem. - const flushedBuckets: MetricBucket = new Map(); - for (const [key, bucket] of this._buckets) { - if (bucket.timestamp <= cutoffSeconds) { - flushedBuckets.set(key, bucket); - this._bucketsTotalWeight -= bucket.metric.weight; - } - } - - for (const [key] of flushedBuckets) { - this._buckets.delete(key); - } - - this._captureMetrics(flushedBuckets); - } - - /** - * Only captures a subset of the buckets passed to this function. - * @param flushedBuckets - */ - private _captureMetrics(flushedBuckets: MetricBucket): void { - if (flushedBuckets.size > 0) { - // TODO(@anonrig): Optimization opportunity. - // This copy operation can be avoided if we store the key in the bucketItem. - const buckets = Array.from(flushedBuckets).map(([, bucketItem]) => bucketItem); - captureAggregateMetrics(this._client, buckets); - } - } -} diff --git a/packages/core/src/metrics/browser-aggregator.ts b/packages/core/src/metrics/browser-aggregator.ts deleted file mode 100644 index fca72f48f40f..000000000000 --- a/packages/core/src/metrics/browser-aggregator.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { Client, MeasurementUnit, MetricsAggregator, Primitive } from '../types-hoist'; -import { timestampInSeconds } from '../utils-hoist/time'; -import { updateMetricSummaryOnActiveSpan } from '../utils/spanUtils'; -import { DEFAULT_BROWSER_FLUSH_INTERVAL, SET_METRIC_TYPE } from './constants'; -import { captureAggregateMetrics } from './envelope'; -import { METRIC_MAP } from './instance'; -import type { MetricBucket, MetricType } from './types'; -import { getBucketKey, sanitizeMetricKey, sanitizeTags, sanitizeUnit } from './utils'; - -/** - * A simple metrics aggregator that aggregates metrics in memory and flushes them periodically. - * Default flush interval is 5 seconds. - * - * @experimental This API is experimental and might change in the future. - */ -export class BrowserMetricsAggregator implements MetricsAggregator { - // TODO(@anonrig): Use FinalizationRegistry to have a proper way of flushing the buckets - // when the aggregator is garbage collected. - // Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry - private _buckets: MetricBucket; - private readonly _interval: ReturnType; - - public constructor(private readonly _client: Client) { - this._buckets = new Map(); - this._interval = setInterval(() => this.flush(), DEFAULT_BROWSER_FLUSH_INTERVAL); - } - - /** - * @inheritDoc - */ - public add( - metricType: MetricType, - unsanitizedName: string, - value: number | string, - unsanitizedUnit: MeasurementUnit | undefined = 'none', - unsanitizedTags: Record | undefined = {}, - maybeFloatTimestamp: number | undefined = timestampInSeconds(), - ): void { - const timestamp = Math.floor(maybeFloatTimestamp); - const name = sanitizeMetricKey(unsanitizedName); - const tags = sanitizeTags(unsanitizedTags); - const unit = sanitizeUnit(unsanitizedUnit as string); - - const bucketKey = getBucketKey(metricType, name, unit, tags); - - let bucketItem = this._buckets.get(bucketKey); - // If this is a set metric, we need to calculate the delta from the previous weight. - const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0; - - if (bucketItem) { - bucketItem.metric.add(value); - // TODO(abhi): Do we need this check? - if (bucketItem.timestamp < timestamp) { - bucketItem.timestamp = timestamp; - } - } else { - bucketItem = { - // @ts-expect-error we don't need to narrow down the type of value here, saves bundle size. - metric: new METRIC_MAP[metricType](value), - timestamp, - metricType, - name, - unit, - tags, - }; - this._buckets.set(bucketKey, bucketItem); - } - - // If value is a string, it's a set metric so calculate the delta from the previous weight. - const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value; - updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey); - } - - /** - * @inheritDoc - */ - public flush(): void { - // short circuit if buckets are empty. - if (this._buckets.size === 0) { - return; - } - - const metricBuckets = Array.from(this._buckets.values()); - captureAggregateMetrics(this._client, metricBuckets); - - this._buckets.clear(); - } - - /** - * @inheritDoc - */ - public close(): void { - clearInterval(this._interval); - this.flush(); - } -} diff --git a/packages/core/src/metrics/constants.ts b/packages/core/src/metrics/constants.ts deleted file mode 100644 index ae1cd968723c..000000000000 --- a/packages/core/src/metrics/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const COUNTER_METRIC_TYPE = 'c' as const; -export const GAUGE_METRIC_TYPE = 'g' as const; -export const SET_METRIC_TYPE = 's' as const; -export const DISTRIBUTION_METRIC_TYPE = 'd' as const; - -/** - * This does not match spec in https://develop.sentry.dev/sdk/metrics - * but was chosen to optimize for the most common case in browser environments. - */ -export const DEFAULT_BROWSER_FLUSH_INTERVAL = 5000; - -/** - * SDKs are required to bucket into 10 second intervals (rollup in seconds) - * which is the current lower bound of metric accuracy. - */ -export const DEFAULT_FLUSH_INTERVAL = 10000; - -/** - * The maximum number of metrics that should be stored in memory. - */ -export const MAX_WEIGHT = 10000; diff --git a/packages/core/src/metrics/envelope.ts b/packages/core/src/metrics/envelope.ts deleted file mode 100644 index 7c1a4d612577..000000000000 --- a/packages/core/src/metrics/envelope.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { Client, DsnComponents, MetricBucketItem, SdkMetadata, StatsdEnvelope, StatsdItem } from '../types-hoist'; -import { dsnToString } from '../utils-hoist/dsn'; -import { createEnvelope } from '../utils-hoist/envelope'; -import { logger } from '../utils-hoist/logger'; -import { serializeMetricBuckets } from './utils'; - -/** - * Captures aggregated metrics to the supplied client. - */ -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(); - const tunnel = client.getOptions().tunnel; - - const metricsEnvelope = createMetricEnvelope(metricBucketItems, dsn, metadata, tunnel); - - // sendEnvelope should not throw - // eslint-disable-next-line @typescript-eslint/no-floating-promises - client.sendEnvelope(metricsEnvelope); -} - -/** - * Create envelope from a metric aggregate. - */ -export function createMetricEnvelope( - metricBucketItems: Array, - dsn?: DsnComponents, - metadata?: SdkMetadata, - tunnel?: string, -): StatsdEnvelope { - const headers: StatsdEnvelope[0] = { - sent_at: new Date().toISOString(), - }; - - if (metadata && metadata.sdk) { - headers.sdk = { - name: metadata.sdk.name, - version: metadata.sdk.version, - }; - } - - if (!!tunnel && dsn) { - headers.dsn = dsnToString(dsn); - } - - const item = createMetricEnvelopeItem(metricBucketItems); - return createEnvelope(headers, [item]); -} - -function createMetricEnvelopeItem(metricBucketItems: MetricBucketItem[]): StatsdItem { - const payload = serializeMetricBuckets(metricBucketItems); - const metricHeaders: StatsdItem[0] = { - type: 'statsd', - length: payload.length, - }; - return [metricHeaders, payload]; -} diff --git a/packages/core/src/metrics/exports-default.ts b/packages/core/src/metrics/exports-default.ts deleted file mode 100644 index e071015b73f1..000000000000 --- a/packages/core/src/metrics/exports-default.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { - Client, - DurationUnit, - MetricData, - Metrics, - MetricsAggregator as MetricsAggregatorInterface, -} from '../types-hoist'; -import { MetricsAggregator } from './aggregator'; -import { metrics as metricsCore } from './exports'; - -/** - * Adds a value to a counter metric - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function increment(name: string, value: number = 1, data?: MetricData): void { - // eslint-disable-next-line deprecation/deprecation - metricsCore.increment(MetricsAggregator, name, value, data); -} - -/** - * Adds a value to a distribution metric - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function distribution(name: string, value: number, data?: MetricData): void { - // eslint-disable-next-line deprecation/deprecation - metricsCore.distribution(MetricsAggregator, name, value, data); -} - -/** - * Adds a value to a set metric. Value must be a string or integer. - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function set(name: string, value: number | string, data?: MetricData): void { - // eslint-disable-next-line deprecation/deprecation - metricsCore.set(MetricsAggregator, name, value, data); -} - -/** - * Adds a value to a gauge metric - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function gauge(name: string, value: number, data?: MetricData): void { - // eslint-disable-next-line deprecation/deprecation - metricsCore.gauge(MetricsAggregator, name, value, data); -} - -/** - * Adds a timing metric. - * The metric is added as a distribution metric. - * - * You can either directly capture a numeric `value`, or wrap a callback function in `timing`. - * In the latter case, the duration of the callback execution will be captured as a span & a metric. - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function timing(name: string, value: number, unit?: DurationUnit, data?: Omit): void; -function timing(name: string, callback: () => T, unit?: DurationUnit, data?: Omit): T; -function timing( - name: string, - value: number | (() => T), - unit: DurationUnit = 'second', - data?: Omit, -): T | void { - // eslint-disable-next-line deprecation/deprecation - return metricsCore.timing(MetricsAggregator, name, value, unit, data); -} - -/** - * Returns the metrics aggregator for a given client. - */ -function getMetricsAggregatorForClient(client: Client): MetricsAggregatorInterface { - // eslint-disable-next-line deprecation/deprecation - return metricsCore.getMetricsAggregatorForClient(client, MetricsAggregator); -} - -/** - * The metrics API is used to capture custom metrics in Sentry. - * - * @deprecated The Sentry metrics beta has ended. This export will be removed in a future release. - */ -export const metricsDefault: Metrics & { - getMetricsAggregatorForClient: typeof getMetricsAggregatorForClient; -} = { - increment, - distribution, - set, - gauge, - timing, - /** - * @ignore This is for internal use only. - */ - getMetricsAggregatorForClient, -}; diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts deleted file mode 100644 index 00f100bcaeb2..000000000000 --- a/packages/core/src/metrics/exports.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { getClient } from '../currentScopes'; -import { DEBUG_BUILD } from '../debug-build'; -import { startSpanManual } from '../tracing'; -import type { Client, DurationUnit, MetricData, MetricsAggregator as MetricsAggregatorInterface } from '../types-hoist'; -import { logger } from '../utils-hoist/logger'; -import { timestampInSeconds } from '../utils-hoist/time'; -import { getGlobalSingleton } from '../utils-hoist/worldwide'; -import { handleCallbackErrors } from '../utils/handleCallbackErrors'; -import { getActiveSpan, getRootSpan, spanToJSON } from '../utils/spanUtils'; -import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants'; -import type { MetricType } from './types'; - -type MetricsAggregatorConstructor = { - new (client: Client): MetricsAggregatorInterface; -}; - -/** - * 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. - */ -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, - metricType: MetricType, - name: string, - value: number | string, - data: MetricData | undefined = {}, -): void { - const client = data.client || getClient(); - - if (!client) { - return; - } - - const span = getActiveSpan(); - const rootSpan = span ? getRootSpan(span) : undefined; - const transactionName = rootSpan && spanToJSON(rootSpan).description; - - const { unit, tags, timestamp } = data; - const { release, environment } = client.getOptions(); - const metricTags: Record = {}; - if (release) { - metricTags.release = release; - } - if (environment) { - metricTags.environment = environment; - } - if (transactionName) { - metricTags.transaction = transactionName; - } - - 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); -} - -/** - * Adds a value to a counter metric - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function increment(aggregator: MetricsAggregatorConstructor, name: string, value: number = 1, data?: MetricData): void { - addToMetricsAggregator(aggregator, COUNTER_METRIC_TYPE, name, ensureNumber(value), data); -} - -/** - * Adds a value to a distribution metric - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function distribution(aggregator: MetricsAggregatorConstructor, name: string, value: number, data?: MetricData): void { - addToMetricsAggregator(aggregator, DISTRIBUTION_METRIC_TYPE, name, ensureNumber(value), data); -} - -/** - * Adds a timing metric. - * The metric is added as a distribution metric. - * - * You can either directly capture a numeric `value`, or wrap a callback function in `timing`. - * In the latter case, the duration of the callback execution will be captured as a span & a metric. - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function timing( - aggregator: MetricsAggregatorConstructor, - name: string, - value: number | (() => T), - unit: DurationUnit = 'second', - data?: Omit, -): T | void { - // callback form - if (typeof value === 'function') { - const startTime = timestampInSeconds(); - - return startSpanManual( - { - op: 'metrics.timing', - name, - startTime, - onlyIfParent: true, - }, - span => { - return handleCallbackErrors( - () => value(), - () => { - // no special error handling necessary - }, - () => { - const endTime = timestampInSeconds(); - const timeDiff = endTime - startTime; - // eslint-disable-next-line deprecation/deprecation - distribution(aggregator, name, timeDiff, { ...data, unit: 'second' }); - span.end(endTime); - }, - ); - }, - ); - } - - // value form - // eslint-disable-next-line deprecation/deprecation - distribution(aggregator, name, value, { ...data, unit }); -} - -/** - * Adds a value to a set metric. Value must be a string or integer. - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function set(aggregator: MetricsAggregatorConstructor, name: string, value: number | string, data?: MetricData): void { - addToMetricsAggregator(aggregator, SET_METRIC_TYPE, name, value, data); -} - -/** - * Adds a value to a gauge metric - * - * @deprecated The Sentry metrics beta has ended. This method will be removed in a future release. - */ -function gauge(aggregator: MetricsAggregatorConstructor, name: string, value: number, data?: MetricData): void { - addToMetricsAggregator(aggregator, GAUGE_METRIC_TYPE, name, ensureNumber(value), data); -} - -/** - * The metrics API is used to capture custom metrics in Sentry. - * - * @deprecated The Sentry metrics beta has ended. This export will be removed in a future release. - */ -export const metrics = { - increment, - distribution, - set, - gauge, - timing, - /** - * @ignore This is for internal use only. - */ - getMetricsAggregatorForClient, -}; - -// Although this is typed to be a number, we try to handle strings as well here -function ensureNumber(number: number | string): number { - return typeof number === 'string' ? parseInt(number) : number; -} diff --git a/packages/core/src/metrics/instance.ts b/packages/core/src/metrics/instance.ts deleted file mode 100644 index 28b57ae7f75c..000000000000 --- a/packages/core/src/metrics/instance.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { MetricInstance } from '../types-hoist'; -import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants'; -import { simpleHash } from './utils'; - -/** - * A metric instance representing a counter. - */ -export class CounterMetric implements MetricInstance { - public constructor(private _value: number) {} - - /** @inheritDoc */ - public get weight(): number { - return 1; - } - - /** @inheritdoc */ - public add(value: number): void { - this._value += value; - } - - /** @inheritdoc */ - public toString(): string { - return `${this._value}`; - } -} - -/** - * A metric instance representing a gauge. - */ -export class GaugeMetric implements MetricInstance { - private _last: number; - private _min: number; - private _max: number; - private _sum: number; - private _count: number; - - public constructor(value: number) { - this._last = value; - this._min = value; - this._max = value; - this._sum = value; - this._count = 1; - } - - /** @inheritDoc */ - public get weight(): number { - return 5; - } - - /** @inheritdoc */ - public add(value: number): void { - this._last = value; - if (value < this._min) { - this._min = value; - } - if (value > this._max) { - this._max = value; - } - this._sum += value; - this._count++; - } - - /** @inheritdoc */ - public toString(): string { - return `${this._last}:${this._min}:${this._max}:${this._sum}:${this._count}`; - } -} - -/** - * A metric instance representing a distribution. - */ -export class DistributionMetric implements MetricInstance { - private _value: number[]; - - public constructor(first: number) { - this._value = [first]; - } - - /** @inheritDoc */ - public get weight(): number { - return this._value.length; - } - - /** @inheritdoc */ - public add(value: number): void { - this._value.push(value); - } - - /** @inheritdoc */ - public toString(): string { - return this._value.join(':'); - } -} - -/** - * A metric instance representing a set. - */ -export class SetMetric implements MetricInstance { - private _value: Set; - - public constructor(public first: number | string) { - this._value = new Set([first]); - } - - /** @inheritDoc */ - public get weight(): number { - return this._value.size; - } - - /** @inheritdoc */ - public add(value: number | string): void { - this._value.add(value); - } - - /** @inheritdoc */ - public toString(): string { - return Array.from(this._value) - .map(val => (typeof val === 'string' ? simpleHash(val) : val)) - .join(':'); - } -} - -export const METRIC_MAP = { - [COUNTER_METRIC_TYPE]: CounterMetric, - [GAUGE_METRIC_TYPE]: GaugeMetric, - [DISTRIBUTION_METRIC_TYPE]: DistributionMetric, - [SET_METRIC_TYPE]: SetMetric, -}; diff --git a/packages/core/src/metrics/metric-summary.ts b/packages/core/src/metrics/metric-summary.ts deleted file mode 100644 index e7a8a00c289a..000000000000 --- a/packages/core/src/metrics/metric-summary.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { MeasurementUnit, Span } from '../types-hoist'; -import type { MetricSummary } from '../types-hoist'; -import type { Primitive } from '../types-hoist'; -import { dropUndefinedKeys } from '../utils-hoist/object'; -import type { MetricType } from './types'; - -/** - * key: bucketKey - * value: [exportKey, MetricSummary] - */ -type MetricSummaryStorage = Map; - -const METRICS_SPAN_FIELD = '_sentryMetrics'; - -type SpanWithPotentialMetrics = Span & { - [METRICS_SPAN_FIELD]?: MetricSummaryStorage; -}; - -/** - * Fetches the metric summary if it exists for the passed span - */ -export function getMetricSummaryJsonForSpan(span: Span): Record> | undefined { - const storage = (span as SpanWithPotentialMetrics)[METRICS_SPAN_FIELD]; - - if (!storage) { - return undefined; - } - const output: Record> = {}; - - for (const [, [exportKey, summary]] of storage) { - const arr = output[exportKey] || (output[exportKey] = []); - arr.push(dropUndefinedKeys(summary)); - } - - return output; -} - -/** - * Updates the metric summary on a span. - */ -export function updateMetricSummaryOnSpan( - span: Span, - metricType: MetricType, - sanitizedName: string, - value: number, - unit: MeasurementUnit, - tags: Record, - bucketKey: string, -): void { - const existingStorage = (span as SpanWithPotentialMetrics)[METRICS_SPAN_FIELD]; - const storage = - existingStorage || - ((span as SpanWithPotentialMetrics)[METRICS_SPAN_FIELD] = new Map()); - - 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, - }, - ]); - } -} diff --git a/packages/core/src/metrics/types.ts b/packages/core/src/metrics/types.ts deleted file mode 100644 index d1d01cd1abab..000000000000 --- a/packages/core/src/metrics/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { MetricBucketItem } from '../types-hoist'; -import type { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants'; - -export type MetricType = - | typeof COUNTER_METRIC_TYPE - | typeof GAUGE_METRIC_TYPE - | typeof SET_METRIC_TYPE - | typeof DISTRIBUTION_METRIC_TYPE; - -// TODO(@anonrig): Convert this to WeakMap when we support ES6 and -// use FinalizationRegistry to flush the buckets when the aggregator is garbage collected. -export type MetricBucket = Map; diff --git a/packages/core/src/metrics/utils.ts b/packages/core/src/metrics/utils.ts deleted file mode 100644 index 903a185e27f3..000000000000 --- a/packages/core/src/metrics/utils.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { MeasurementUnit, MetricBucketItem, Primitive } from '../types-hoist'; -import { dropUndefinedKeys } from '../utils-hoist/object'; -import type { MetricType } from './types'; - -/** - * Generate bucket key from metric properties. - */ -export function getBucketKey( - metricType: MetricType, - name: string, - unit: MeasurementUnit, - tags: Record, -): string { - const stringifiedTags = Object.entries(dropUndefinedKeys(tags)).sort((a, b) => a[0].localeCompare(b[0])); - return `${metricType}${name}${unit}${stringifiedTags}`; -} - -/* eslint-disable no-bitwise */ -/** - * Simple hash function for strings. - */ -export function simpleHash(s: string): number { - let rv = 0; - for (let i = 0; i < s.length; i++) { - const c = s.charCodeAt(i); - rv = (rv << 5) - rv + c; - rv &= rv; - } - return rv >>> 0; -} -/* eslint-enable no-bitwise */ - -/** - * Serialize metrics buckets into a string based on statsd format. - * - * Example of format: - * metric.name@second:1:1.2|d|#a:value,b:anothervalue|T12345677 - * Segments: - * name: metric.name - * unit: second - * value: [1, 1.2] - * type of metric: d (distribution) - * tags: { a: value, b: anothervalue } - * timestamp: 12345677 - */ -export function serializeMetricBuckets(metricBucketItems: MetricBucketItem[]): string { - let out = ''; - for (const item of metricBucketItems) { - const tagEntries = Object.entries(item.tags); - const maybeTags = tagEntries.length > 0 ? `|#${tagEntries.map(([key, value]) => `${key}:${value}`).join(',')}` : ''; - out += `${item.name}@${item.unit}:${item.metric}|${item.metricType}${maybeTags}|T${item.timestamp}\n`; - } - return out; -} - -/** - * Sanitizes units - * - * These Regex's are straight from the normalisation docs: - * https://develop.sentry.dev/sdk/metrics/#normalization - */ -export function sanitizeUnit(unit: string): string { - return unit.replace(/[^\w]+/gi, '_'); -} - -/** - * Sanitizes metric keys - * - * These Regex's are straight from the normalisation docs: - * https://develop.sentry.dev/sdk/metrics/#normalization - */ -export function sanitizeMetricKey(key: string): string { - return key.replace(/[^\w\-.]+/gi, '_'); -} - -/** - * Sanitizes metric keys - * - * These Regex's are straight from the normalisation docs: - * https://develop.sentry.dev/sdk/metrics/#normalization - */ -function sanitizeTagKey(key: string): string { - return key.replace(/[^\w\-./]+/gi, ''); -} - -/** - * These Regex's are straight from the normalisation docs: - * https://develop.sentry.dev/sdk/metrics/#normalization - */ -const tagValueReplacements: [string, string][] = [ - ['\n', '\\n'], - ['\r', '\\r'], - ['\t', '\\t'], - ['\\', '\\\\'], - ['|', '\\u{7c}'], - [',', '\\u{2c}'], -]; - -function getCharOrReplacement(input: string): string { - for (const [search, replacement] of tagValueReplacements) { - if (input === search) { - return replacement; - } - } - - return input; -} - -function sanitizeTagValue(value: string): string { - return [...value].reduce((acc, char) => acc + getCharOrReplacement(char), ''); -} - -/** - * Sanitizes tags. - */ -export function sanitizeTags(unsanitizedTags: Record): Record { - const tags: Record = {}; - for (const key in unsanitizedTags) { - if (Object.prototype.hasOwnProperty.call(unsanitizedTags, key)) { - const sanitizedKey = sanitizeTagKey(key); - tags[sanitizedKey] = sanitizeTagValue(String(unsanitizedTags[key])); - } - } - return tags; -} diff --git a/packages/core/src/report-dialog.ts b/packages/core/src/report-dialog.ts new file mode 100644 index 000000000000..fb91aec441b2 --- /dev/null +++ b/packages/core/src/report-dialog.ts @@ -0,0 +1,29 @@ +import type { DsnLike } from './types-hoist/dsn'; + +/** + * All properties the report dialog supports + */ +export interface ReportDialogOptions extends Record { + eventId?: string; + dsn?: DsnLike; + user?: { + email?: string; + name?: string; + }; + lang?: string; + title?: string; + subtitle?: string; + subtitle2?: string; + labelName?: string; + labelEmail?: string; + labelComments?: string; + labelClose?: string; + labelSubmit?: string; + errorGeneric?: string; + errorFormEntry?: string; + successMessage?: string; + /** Callback after reportDialog showed up */ + onLoad?(this: void): void; + /** Callback after reportDialog closed */ + onClose?(this: void): void; +} diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 5bba8615e876..ce559d589fe3 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -1,11 +1,12 @@ /* eslint-disable max-lines */ +import type { Client } from './client'; +import { updateSession } from './session'; import type { Attachment, Breadcrumb, - CaptureContext, - Client, Context, Contexts, + DynamicSamplingContext, Event, EventHint, EventProcessor, @@ -13,20 +14,16 @@ import type { Extras, Primitive, PropagationContext, - RequestSession, - Scope as ScopeInterface, - ScopeContext, - ScopeData, + RequestEventData, Session, SeverityLevel, + Span, User, } from './types-hoist'; - -import { updateSession } from './session'; import { isPlainObject } from './utils-hoist/is'; import { logger } from './utils-hoist/logger'; import { uuid4 } from './utils-hoist/misc'; -import { generateSpanId, generateTraceId } from './utils-hoist/propagationContext'; +import { generateTraceId } from './utils-hoist/propagationContext'; import { dateTimestampInSeconds } from './utils-hoist/time'; import { merge } from './utils/merge'; import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope'; @@ -36,10 +33,62 @@ import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope'; */ const DEFAULT_MAX_BREADCRUMBS = 100; +/** + * A context to be used for capturing an event. + * This can either be a Scope, or a partial ScopeContext, + * or a callback that receives the current scope and returns a new scope to use. + */ +export type CaptureContext = Scope | Partial | ((scope: Scope) => Scope); + +/** + * Data that can be converted to a Scope. + */ +export interface ScopeContext { + user: User; + level: SeverityLevel; + extra: Extras; + contexts: Contexts; + tags: { [key: string]: Primitive }; + fingerprint: string[]; + propagationContext: PropagationContext; +} + +export interface SdkProcessingMetadata { + [key: string]: unknown; + requestSession?: { + status: 'ok' | 'errored' | 'crashed'; + }; + normalizedRequest?: RequestEventData; + dynamicSamplingContext?: Partial; + capturedSpanScope?: Scope; + capturedSpanIsolationScope?: Scope; + spanCountBeforeProcessing?: number; + ipAddress?: string; +} + +/** + * Normalized data of the Scope, ready to be used. + */ +export interface ScopeData { + eventProcessors: EventProcessor[]; + breadcrumbs: Breadcrumb[]; + user: User; + tags: { [key: string]: Primitive }; + extra: Extras; + contexts: Contexts; + attachments: Attachment[]; + propagationContext: PropagationContext; + sdkProcessingMetadata: SdkProcessingMetadata; + fingerprint: string[]; + level?: SeverityLevel; + transactionName?: string; + span?: Span; +} + /** * Holds additional event information. */ -class ScopeClass implements ScopeInterface { +export class Scope { /** Flag if notifying is happening. */ protected _notifyingListeners: boolean; @@ -74,7 +123,7 @@ class ScopeClass implements ScopeInterface { * A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get * sent to Sentry */ - protected _sdkProcessingMetadata: { [key: string]: unknown }; + protected _sdkProcessingMetadata: SdkProcessingMetadata; /** Fingerprint */ protected _fingerprint?: string[]; @@ -93,10 +142,6 @@ class ScopeClass implements ScopeInterface { /** Session */ protected _session?: Session; - /** Request Mode Session Status */ - // eslint-disable-next-line deprecation/deprecation - protected _requestSession?: RequestSession; - /** The client on this scope */ protected _client?: Client; @@ -118,15 +163,15 @@ class ScopeClass implements ScopeInterface { this._sdkProcessingMetadata = {}; this._propagationContext = { traceId: generateTraceId(), - spanId: generateSpanId(), + sampleRand: Math.random(), }; } /** - * @inheritDoc + * Clone all data from this scope into a new scope. */ - public clone(): ScopeClass { - const newScope = new ScopeClass(); + public clone(): Scope { + const newScope = new Scope(); newScope._breadcrumbs = [...this._breadcrumbs]; newScope._tags = { ...this._tags }; newScope._extra = { ...this._extra }; @@ -145,7 +190,6 @@ class ScopeClass implements ScopeInterface { newScope._transactionName = this._transactionName; newScope._fingerprint = this._fingerprint; newScope._eventProcessors = [...this._eventProcessors]; - newScope._requestSession = this._requestSession; newScope._attachments = [...this._attachments]; newScope._sdkProcessingMetadata = { ...this._sdkProcessingMetadata }; newScope._propagationContext = { ...this._propagationContext }; @@ -158,28 +202,32 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Update the client assigned to this scope. + * Note that not every scope will have a client assigned - isolation scopes & the global scope will generally not have a client, + * as well as manually created scopes. */ public setClient(client: Client | undefined): void { this._client = client; } /** - * @inheritDoc + * Set the ID of the last captured error event. + * This is generally only captured on the isolation scope. */ public setLastEventId(lastEventId: string | undefined): void { this._lastEventId = lastEventId; } /** - * @inheritDoc + * Get the client assigned to this scope. */ public getClient(): C | undefined { return this._client as C | undefined; } /** - * @inheritDoc + * Get the ID of the last captured error event. + * This is generally only available on the isolation scope. */ public lastEventId(): string | undefined { return this._lastEventId; @@ -193,7 +241,7 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Add an event processor that will be called before an event is sent. */ public addEventProcessor(callback: EventProcessor): this { this._eventProcessors.push(callback); @@ -201,7 +249,8 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Set the user for this scope. + * Set to `null` to unset the user. */ public setUser(user: User | null): this { // If null is passed we want to unset everything, but still define keys, @@ -222,31 +271,15 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Get the user from this scope. */ public getUser(): User | undefined { return this._user; } /** - * @inheritDoc - */ - // eslint-disable-next-line deprecation/deprecation - public getRequestSession(): RequestSession | undefined { - return this._requestSession; - } - - /** - * @inheritDoc - */ - // eslint-disable-next-line deprecation/deprecation - public setRequestSession(requestSession?: RequestSession): this { - this._requestSession = requestSession; - return this; - } - - /** - * @inheritDoc + * Set an object that will be merged into existing tags on the scope, + * and will be sent as tags data with the event. */ public setTags(tags: { [key: string]: Primitive }): this { this._tags = { @@ -258,7 +291,7 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Set a single tag that will be sent as tags data with the event. */ public setTag(key: string, value: Primitive): this { this._tags = { ...this._tags, [key]: value }; @@ -267,7 +300,8 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Set an object that will be merged into existing extra on the scope, + * and will be sent as extra data with the event. */ public setExtras(extras: Extras): this { this._extra = { @@ -279,7 +313,7 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Set a single key:value extra entry that will be sent as extra data with the event. */ public setExtra(key: string, extra: Extra): this { this._extra = { ...this._extra, [key]: extra }; @@ -288,7 +322,8 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Sets the fingerprint on the scope to send with the events. + * @param {string[]} fingerprint Fingerprint to group events in Sentry. */ public setFingerprint(fingerprint: string[]): this { this._fingerprint = fingerprint; @@ -297,7 +332,7 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Sets the level on the scope for future events. */ public setLevel(level: SeverityLevel): this { this._level = level; @@ -306,7 +341,15 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Sets the transaction name on the scope so that the name of e.g. taken server route or + * the page location is attached to future events. + * + * IMPORTANT: Calling this function does NOT change the name of the currently active + * root span. If you want to change the name of the active root span, use + * `Sentry.updateSpanName(rootSpan, 'new name')` instead. + * + * By default, the SDK updates the scope's transaction name automatically on sensible + * occasions, such as a page navigation or when handling a new request on the server. */ public setTransactionName(name?: string): this { this._transactionName = name; @@ -315,7 +358,9 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Sets context data with the given name. + * Data passed as context will be normalized. You can also pass `null` to unset the context. + * Note that context data will not be merged - calling `setContext` will overwrite an existing context with the same key. */ public setContext(key: string, context: Context | null): this { if (context === null) { @@ -330,7 +375,7 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Set the session for the scope. */ public setSession(session?: Session): this { if (!session) { @@ -343,14 +388,17 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Get the session from the scope. */ public getSession(): Session | undefined { return this._session; } /** - * @inheritDoc + * Updates the scope with provided data. Can work in three variations: + * - plain object containing updatable attributes + * - Scope instance that'll extract the attributes from + * - callback function that'll receive the current scope as an argument and allow for modifications */ public update(captureContext?: CaptureContext): this { if (!captureContext) { @@ -359,13 +407,12 @@ class ScopeClass implements ScopeInterface { const scopeToMerge = typeof captureContext === 'function' ? captureContext(this) : captureContext; - const [scopeInstance, requestSession] = + const scopeInstance = scopeToMerge instanceof Scope - ? // eslint-disable-next-line deprecation/deprecation - [scopeToMerge.getScopeData(), scopeToMerge.getRequestSession()] + ? scopeToMerge.getScopeData() : isPlainObject(scopeToMerge) - ? [captureContext as ScopeContext, (captureContext as ScopeContext).requestSession] - : []; + ? (captureContext as ScopeContext) + : undefined; const { tags, extra, user, contexts, level, fingerprint = [], propagationContext } = scopeInstance || {}; @@ -389,15 +436,12 @@ class ScopeClass implements ScopeInterface { this._propagationContext = propagationContext; } - if (requestSession) { - this._requestSession = requestSession; - } - return this; } /** - * @inheritDoc + * Clears the current scope and resets its properties. + * Note: The client will not be cleared. */ public clear(): this { // client is not cleared here on purpose! @@ -409,18 +453,18 @@ class ScopeClass implements ScopeInterface { this._level = undefined; this._transactionName = undefined; this._fingerprint = undefined; - this._requestSession = undefined; this._session = undefined; _setSpanForScope(this, undefined); this._attachments = []; - this.setPropagationContext({ traceId: generateTraceId() }); + this.setPropagationContext({ traceId: generateTraceId(), sampleRand: Math.random() }); this._notifyScopeListeners(); return this; } /** - * @inheritDoc + * Adds a breadcrumb to the scope. + * By default, the last 100 breadcrumbs are kept. */ public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { const maxCrumbs = typeof maxBreadcrumbs === 'number' ? maxBreadcrumbs : DEFAULT_MAX_BREADCRUMBS; @@ -445,14 +489,14 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Get the last breadcrumb of the scope. */ public getLastBreadcrumb(): Breadcrumb | undefined { return this._breadcrumbs[this._breadcrumbs.length - 1]; } /** - * @inheritDoc + * Clear all breadcrumbs from the scope. */ public clearBreadcrumbs(): this { this._breadcrumbs = []; @@ -461,7 +505,7 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Add an attachment to the scope. */ public addAttachment(attachment: Attachment): this { this._attachments.push(attachment); @@ -469,14 +513,16 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Clear all attachments from the scope. */ public clearAttachments(): this { this._attachments = []; return this; } - /** @inheritDoc */ + /** + * Get the data of this scope, which should be applied to an event during processing. + */ public getScopeData(): ScopeData { return { breadcrumbs: this._breadcrumbs, @@ -496,39 +542,35 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Add data which will be accessible during event processing but won't get sent to Sentry. */ - public setSDKProcessingMetadata(newData: { [key: string]: unknown }): this { + public setSDKProcessingMetadata(newData: SdkProcessingMetadata): this { this._sdkProcessingMetadata = merge(this._sdkProcessingMetadata, newData, 2); return this; } /** - * @inheritDoc + * Add propagation context to the scope, used for distributed tracing */ - public setPropagationContext( - context: Omit & Partial>, - ): this { - this._propagationContext = { - // eslint-disable-next-line deprecation/deprecation - spanId: generateSpanId(), - ...context, - }; + public setPropagationContext(context: PropagationContext): this { + this._propagationContext = context; return this; } /** - * @inheritDoc + * Get propagation context from the scope, used for distributed tracing */ public getPropagationContext(): PropagationContext { return this._propagationContext; } /** - * @inheritDoc + * Capture an exception for this scope. + * + * @returns {string} The id of the captured Sentry event. */ public captureException(exception: unknown, hint?: EventHint): string { - const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + const eventId = hint?.event_id || uuid4(); if (!this._client) { logger.warn('No client configured on scope - will not capture exception!'); @@ -552,10 +594,12 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Capture a message for this scope. + * + * @returns {string} The id of the captured message. */ public captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string { - const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + const eventId = hint?.event_id || uuid4(); if (!this._client) { logger.warn('No client configured on scope - will not capture message!'); @@ -580,10 +624,12 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Capture a Sentry event for this scope. + * + * @returns {string} The id of the captured event. */ public captureEvent(event: Event, hint?: EventHint): string { - const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + const eventId = hint?.event_id || uuid4(); if (!this._client) { logger.warn('No client configured on scope - will not capture event!'); @@ -611,13 +657,3 @@ class ScopeClass implements ScopeInterface { } } } - -/** - * Holds additional event information. - */ -export const Scope = ScopeClass; - -/** - * Holds additional event information. - */ -export type Scope = ScopeInterface; diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index 64037fa37d5c..2665e91ec938 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -1,7 +1,7 @@ +import type { Client } from './client'; import { getCurrentScope } from './currentScopes'; -import type { Client, ClientOptions } from './types-hoist'; - import { DEBUG_BUILD } from './debug-build'; +import type { ClientOptions } from './types-hoist'; import { consoleSandbox, logger } from './utils-hoist/logger'; /** A class object that can instantiate Client objects. */ diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index 2896bd81f93f..dea57836d3bc 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -6,7 +6,10 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_SOURCE = 'sentry.source'; /** - * Use this attribute to represent the sample rate used for a span. + * Attributes that holds the sample rate that was locally applied to a span. + * If this attribute is not defined, it means that the span inherited a sampling decision. + * + * NOTE: Is only defined on root spans. */ export const SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE = 'sentry.sample_rate'; @@ -29,6 +32,15 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT = 'sentry.measurement_un /** The value of a measurement, which may be stored as a TimedEvent. */ export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE = 'sentry.measurement_value'; +/** + * A custom span name set by users guaranteed to be taken over any automatically + * inferred name. This attribute is removed before the span is sent. + * + * @internal only meant for internal SDK usage + * @hidden + */ +export const SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME = 'sentry.custom_span_name'; + /** * The id of the profile that this span occurred in. */ diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index e1d89c1d067b..8bb07b976d65 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -12,12 +12,11 @@ import type { TraceContext, } from './types-hoist'; -import { BaseClient } from './baseclient'; import { createCheckInEnvelope } from './checkin'; +import { Client } from './client'; import { getIsolationScope, getTraceContextFromScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { Scope } from './scope'; -import { SessionFlusher } from './sessionflusher'; import { getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan, @@ -41,10 +40,7 @@ export interface ServerRuntimeClientOptions extends ClientOptions extends BaseClient { - // eslint-disable-next-line deprecation/deprecation - protected _sessionFlusher: SessionFlusher | undefined; - +> extends Client { /** * Creates a new Edge SDK instance. * @param options Configuration options for this SDK. @@ -83,22 +79,7 @@ export class ServerRuntimeClient< * @inheritDoc */ public captureException(exception: unknown, hint?: EventHint, scope?: Scope): string { - // Check if `_sessionFlusher` exists because it is initialized (defined) only when the `autoSessionTracking` is enabled. - // The expectation is that session aggregates are only sent when `autoSessionTracking` is enabled. - // TODO(v9): Our goal in the future is to not have the `autoSessionTracking` option and instead rely on integrations doing the creation and sending of sessions. We will not have a central kill-switch for sessions. - // TODO(v9): This should move into the httpIntegration. - // eslint-disable-next-line deprecation/deprecation - if (this._options.autoSessionTracking && this._sessionFlusher) { - // eslint-disable-next-line deprecation/deprecation - const requestSession = getIsolationScope().getRequestSession(); - - // Necessary checks to ensure this is code block is executed only within a request - // Should override the status only if `requestSession.status` is `Ok`, which is its initial stage - if (requestSession && requestSession.status === 'ok') { - requestSession.status = 'errored'; - } - } - + setCurrentRequestSessionErroredOrCrashed(hint); return super.captureException(exception, hint, scope); } @@ -106,63 +87,15 @@ export class ServerRuntimeClient< * @inheritDoc */ public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string { - // Check if `_sessionFlusher` exists because it is initialized only when the `autoSessionTracking` is enabled. - // The expectation is that session aggregates are only sent when `autoSessionTracking` is enabled. - // TODO(v9): Our goal in the future is to not have the `autoSessionTracking` option and instead rely on integrations doing the creation and sending of sessions. We will not have a central kill-switch for sessions. - // TODO(v9): This should move into the httpIntegration. - // eslint-disable-next-line deprecation/deprecation - if (this._options.autoSessionTracking && this._sessionFlusher) { - const eventType = event.type || 'exception'; - const isException = - eventType === 'exception' && event.exception && event.exception.values && event.exception.values.length > 0; - - // If the event is of type Exception, then a request session should be captured - if (isException) { - // eslint-disable-next-line deprecation/deprecation - const requestSession = getIsolationScope().getRequestSession(); - - // Ensure that this is happening within the bounds of a request, and make sure not to override - // Session Status if Errored / Crashed - if (requestSession && requestSession.status === 'ok') { - requestSession.status = 'errored'; - } - } + // If the event is of type Exception, then a request session should be captured + const isException = !event.type && event.exception?.values && event.exception.values.length > 0; + if (isException) { + setCurrentRequestSessionErroredOrCrashed(hint); } return super.captureEvent(event, hint, scope); } - /** - * - * @inheritdoc - */ - public close(timeout?: number): PromiseLike { - if (this._sessionFlusher) { - this._sessionFlusher.close(); - } - return super.close(timeout); - } - - /** - * Initializes an instance of SessionFlusher on the client which will aggregate and periodically flush session data. - * - * NOTICE: This method will implicitly create an interval that is periodically called. - * To clean up this resources, call `.close()` when you no longer intend to use the client. - * Not doing so will result in a memory leak. - */ - public initSessionFlusher(): void { - const { release, environment } = this._options; - if (!release) { - DEBUG_BUILD && logger.warn('Cannot initialize an instance of SessionFlusher if no release is provided!'); - } else { - // eslint-disable-next-line deprecation/deprecation - this._sessionFlusher = new SessionFlusher(this, { - release, - environment, - }); - } - } - /** * Create a cron monitor check in and send it to Sentry. * @@ -173,7 +106,7 @@ export class ServerRuntimeClient< public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string { const id = 'checkInId' in checkIn && checkIn.checkInId ? checkIn.checkInId : uuid4(); if (!this._isEnabled()) { - DEBUG_BUILD && logger.warn('SDK not enabled, will not capture checkin.'); + DEBUG_BUILD && logger.warn('SDK not enabled, will not capture check-in.'); return id; } @@ -227,28 +160,14 @@ export class ServerRuntimeClient< return id; } - /** - * Method responsible for capturing/ending a request session by calling `incrementSessionStatusCount` to increment - * appropriate session aggregates bucket - * - * @deprecated This method should not be used or extended. It's functionality will move into the `httpIntegration` and not be part of any public API. - */ - protected _captureRequestSession(): void { - if (!this._sessionFlusher) { - DEBUG_BUILD && logger.warn('Discarded request mode session because autoSessionTracking option was disabled'); - } else { - this._sessionFlusher.incrementSessionStatusCount(); - } - } - /** * @inheritDoc */ protected _prepareEvent( event: Event, hint: EventHint, - scope?: Scope, - isolationScope?: Scope, + currentScope: Scope, + isolationScope: Scope, ): PromiseLike { if (this._options.platform) { event.platform = event.platform || this._options.platform; @@ -257,7 +176,7 @@ export class ServerRuntimeClient< if (this._options.runtime) { event.contexts = { ...event.contexts, - runtime: (event.contexts || {}).runtime || this._options.runtime, + runtime: event.contexts?.runtime || this._options.runtime, }; } @@ -265,7 +184,7 @@ export class ServerRuntimeClient< event.server_name = event.server_name || this._options.serverName; } - return super._prepareEvent(event, hint, scope, isolationScope); + return super._prepareEvent(event, hint, currentScope, isolationScope); } /** Extract trace information from scope */ @@ -285,3 +204,20 @@ export class ServerRuntimeClient< return [dynamicSamplingContext, traceContext]; } } + +function setCurrentRequestSessionErroredOrCrashed(eventHint?: EventHint): void { + const requestSession = getIsolationScope().getScopeData().sdkProcessingMetadata.requestSession; + if (requestSession) { + // We mutate instead of doing `setSdkProcessingMetadata` because the http integration stores away a particular + // isolationScope. If that isolation scope is forked, setting the processing metadata here will not mutate the + // original isolation scope that the http integration stored away. + const isHandledException = eventHint?.mechanism?.handled ?? true; + // A request session can go from "errored" -> "crashed" but not "crashed" -> "errored". + // Crashed (unhandled exception) is worse than errored (handled exception). + if (isHandledException && requestSession.status !== 'crashed') { + requestSession.status = 'errored'; + } else if (!isHandledException) { + requestSession.status = 'crashed'; + } + } +} diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 058fd7d68c14..860dec52b386 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -38,7 +38,7 @@ export function makeSession(context?: Omit * Note that this function mutates the passed object and returns void. * (Had to do this instead of returning a new and updated session because closing and sending a session * makes an update to the session after it was passed to the sending logic. - * @see BaseClient.captureSession ) + * @see Client.captureSession ) * * @param session the `Session` to update * @param context the `SessionContext` holding the properties that should be updated in @param session diff --git a/packages/core/src/sessionflusher.ts b/packages/core/src/sessionflusher.ts deleted file mode 100644 index 2434023bf797..000000000000 --- a/packages/core/src/sessionflusher.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { getIsolationScope } from './currentScopes'; -import type { - AggregationCounts, - Client, - RequestSessionStatus, - SessionAggregates, - SessionFlusherLike, -} from './types-hoist'; -import { dropUndefinedKeys } from './utils-hoist/object'; - -type ReleaseHealthAttributes = { - environment?: string; - release: string; -}; - -/** - * @deprecated `SessionFlusher` is deprecated and will be removed in the next major version of the SDK. - */ -// TODO(v9): The goal for the SessionFlusher is to become a stupidly simple mechanism to aggregate "Sessions" (actually "RequestSessions"). It should probably live directly inside the Http integration/instrumentation. -// eslint-disable-next-line deprecation/deprecation -export class SessionFlusher implements SessionFlusherLike { - public readonly flushTimeout: number; - private _pendingAggregates: Map; - private _sessionAttrs: ReleaseHealthAttributes; - // We adjust the type here to add the `unref()` part, as setInterval can technically return a number or a NodeJS.Timer - private readonly _intervalId: ReturnType & { unref?: () => void }; - private _isEnabled: boolean; - private _client: Client; - - public constructor(client: Client, attrs: ReleaseHealthAttributes) { - this._client = client; - this.flushTimeout = 60; - this._pendingAggregates = new Map(); - this._isEnabled = true; - - // Call to setInterval, so that flush is called every 60 seconds. - this._intervalId = setInterval(() => this.flush(), this.flushTimeout * 1000); - if (this._intervalId.unref) { - this._intervalId.unref(); - } - this._sessionAttrs = attrs; - } - - /** Checks if `pendingAggregates` has entries, and if it does flushes them by calling `sendSession` */ - public flush(): void { - const sessionAggregates = this.getSessionAggregates(); - if (sessionAggregates.aggregates.length === 0) { - return; - } - this._pendingAggregates = new Map(); - this._client.sendSession(sessionAggregates); - } - - /** Massages the entries in `pendingAggregates` and returns aggregated sessions */ - public getSessionAggregates(): SessionAggregates { - const aggregates: AggregationCounts[] = Array.from(this._pendingAggregates.values()); - - const sessionAggregates: SessionAggregates = { - attrs: this._sessionAttrs, - aggregates, - }; - return dropUndefinedKeys(sessionAggregates); - } - - /** JSDoc */ - public close(): void { - clearInterval(this._intervalId); - this._isEnabled = false; - this.flush(); - } - - /** - * Wrapper function for _incrementSessionStatusCount that checks if the instance of SessionFlusher is enabled then - * fetches the session status of the request from `Scope.getRequestSession().status` on the scope and passes them to - * `_incrementSessionStatusCount` along with the start date - */ - public incrementSessionStatusCount(): void { - if (!this._isEnabled) { - return; - } - const isolationScope = getIsolationScope(); - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - - if (requestSession && requestSession.status) { - this._incrementSessionStatusCount(requestSession.status, new Date()); - // This is not entirely necessarily but is added as a safe guard to indicate the bounds of a request and so in - // case captureRequestSession is called more than once to prevent double count - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession(undefined); - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ - } - } - - /** - * Increments status bucket in pendingAggregates buffer (internal state) corresponding to status of - * the session received - */ - // eslint-disable-next-line deprecation/deprecation - private _incrementSessionStatusCount(status: RequestSessionStatus, date: Date): number { - // Truncate minutes and seconds on Session Started attribute to have one minute bucket keys - const sessionStartedTrunc = new Date(date).setSeconds(0, 0); - - // corresponds to aggregated sessions in one specific minute bucket - // for example, {"started":"2021-03-16T08:00:00.000Z","exited":4, "errored": 1} - let aggregationCounts = this._pendingAggregates.get(sessionStartedTrunc); - if (!aggregationCounts) { - aggregationCounts = { started: new Date(sessionStartedTrunc).toISOString() }; - this._pendingAggregates.set(sessionStartedTrunc, aggregationCounts); - } - - switch (status) { - case 'errored': - aggregationCounts.errored = (aggregationCounts.errored || 0) + 1; - return aggregationCounts.errored; - case 'ok': - aggregationCounts.exited = (aggregationCounts.exited || 0) + 1; - return aggregationCounts.exited; - default: - aggregationCounts.crashed = (aggregationCounts.crashed || 0) + 1; - return aggregationCounts.crashed; - } - } -} diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index cdf6951fe95b..e6f3ceae79f8 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -1,8 +1,9 @@ -import type { Client, DynamicSamplingContext, Scope, Span } from '../types-hoist'; - +import type { Client } from '../client'; import { DEFAULT_ENVIRONMENT } from '../constants'; import { getClient } from '../currentScopes'; +import type { Scope } from '../scope'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; +import type { DynamicSamplingContext, Span } from '../types-hoist'; import { baggageHeaderToDynamicSamplingContext, dynamicSamplingContextToSentryBaggageHeader, @@ -10,6 +11,7 @@ import { import { addNonEnumerableProperty, dropUndefinedKeys } from '../utils-hoist/object'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; +import { getCapturedScopesOnSpan } from './utils'; /** * If you change this value, also update the terser plugin config to @@ -73,39 +75,45 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly): Partial { + if (typeof rootSpanSampleRate === 'number' || typeof rootSpanSampleRate === 'string') { + dsc.sample_rate = `${rootSpanSampleRate}`; + } + return dsc; + } // For core implementation, we freeze the DSC onto the span as a non-enumerable property const frozenDsc = (rootSpan as SpanWithMaybeDsc)[FROZEN_DSC_FIELD]; if (frozenDsc) { - return frozenDsc; + return applyLocalSampleRateToDsc(frozenDsc); } // For OpenTelemetry, we freeze the DSC on the trace state - const traceState = rootSpan.spanContext().traceState; - const traceStateDsc = traceState && traceState.get('sentry.dsc'); + const traceStateDsc = traceState?.get('sentry.dsc'); // If the span has a DSC, we want it to take precedence const dscOnTraceState = traceStateDsc && baggageHeaderToDynamicSamplingContext(traceStateDsc); if (dscOnTraceState) { - return dscOnTraceState; + return applyLocalSampleRateToDsc(dscOnTraceState); } // Else, we generate it from the span const dsc = getDynamicSamplingContextFromClient(span.spanContext().traceId, client); - const jsonSpan = spanToJSON(rootSpan); - const attributes = jsonSpan.data || {}; - const maybeSampleRate = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]; - - if (maybeSampleRate != null) { - dsc.sample_rate = `${maybeSampleRate}`; - } // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII - const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + const source = rootSpanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; // after JSON conversion, txn.name becomes jsonSpan.description - const name = jsonSpan.description; + const name = rootSpanJson.description; if (source !== 'url' && name) { dsc.transaction = name; } @@ -115,8 +123,18 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly; + freezeDscOnSpan(span, dsc); + + return span; } const scope = getCurrentScope(); @@ -124,6 +134,12 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti beforeSpanEnd(span); } + // If the span is non-recording, nothing more to do here... + // This is the case if tracing is enabled but this specific span was not sampled + if (thisArg instanceof SentryNonRecordingSpan) { + return; + } + // Just ensuring that this keeps working, even if we ever have more arguments here const [definedEndTimestamp, ...rest] = args; const timestamp = definedEndTimestamp || timestampInSeconds(); @@ -255,7 +271,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti return; } - const attributes: SpanAttributes = spanJSON.data || {}; + const attributes = spanJSON.data; if (!attributes[SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]) { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, _finishReason); } diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index a6905741d1c9..c163e263d56d 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -1,7 +1,5 @@ export { registerSpanErrorInstrumentation } from './errors'; export { setCapturedScopesOnSpan, getCapturedScopesOnSpan } from './utils'; -// eslint-disable-next-line deprecation/deprecation -export { addTracingExtensions } from './hubextensions'; export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan'; export { SentrySpan } from './sentrySpan'; export { SentryNonRecordingSpan } from './sentryNonRecordingSpan'; diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 9109e78e0343..eb19643164aa 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -12,26 +12,28 @@ import { parseSampleRate } from '../utils/parseSampleRate'; * sent to Sentry. */ export function sampleSpan( - options: Pick, + options: Pick, samplingContext: SamplingContext, -): [sampled: boolean, sampleRate?: number] { + sampleRand: number, +): [sampled: boolean, sampleRate?: number, localSampleRateWasApplied?: boolean] { // nothing to do if tracing is not enabled if (!hasTracingEnabled(options)) { return [false]; } - // we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` nor `enableTracing` were defined, so one of these should + let localSampleRateWasApplied = undefined; + + // we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` were defined, so one of these should // work; prefer the hook if so let sampleRate; if (typeof options.tracesSampler === 'function') { sampleRate = options.tracesSampler(samplingContext); + localSampleRateWasApplied = true; } else if (samplingContext.parentSampled !== undefined) { sampleRate = samplingContext.parentSampled; } else if (typeof options.tracesSampleRate !== 'undefined') { sampleRate = options.tracesSampleRate; - } else { - // When `enableTracing === true`, we use a sample rate of 100% - sampleRate = 1; + localSampleRateWasApplied = true; } // Since this is coming from the user (or from a function provided by the user), who knows what we might get. @@ -53,12 +55,12 @@ export function sampleSpan( : 'a negative sampling decision was inherited or tracesSampleRate is set to 0' }`, ); - return [false, parsedSampleRate]; + return [false, parsedSampleRate, localSampleRateWasApplied]; } - // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is - // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false. - const shouldSample = Math.random() < parsedSampleRate; + // We always compare the sample rand for the current execution context against the chosen sample rate. + // Read more: https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value + const shouldSample = sampleRand < parsedSampleRate; // if we're not going to keep it, we're done if (!shouldSample) { @@ -68,8 +70,7 @@ export function sampleSpan( sampleRate, )})`, ); - return [false, parsedSampleRate]; } - return [true, parsedSampleRate]; + return [shouldSample, parsedSampleRate, localSampleRateWasApplied]; } diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 126702dfad2b..74478f79903f 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -1,10 +1,10 @@ import { getClient, getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import { createSpanEnvelope } from '../envelope'; -import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, SEMANTIC_ATTRIBUTE_PROFILE_ID, + SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -232,7 +232,6 @@ export class SentrySpan implements Span { timestamp: this._endTime, trace_id: this._traceId, origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, - _metrics_summary: getMetricSummaryJsonForSpan(this), profile_id: this._attributes[SEMANTIC_ATTRIBUTE_PROFILE_ID] as string | undefined, exclusive_time: this._attributes[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME] as number | undefined, measurements: timedEventsToMeasurements(this._events), @@ -334,17 +333,8 @@ export class SentrySpan implements Span { } const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = getCapturedScopesOnSpan(this); - const scope = capturedSpanScope || getCurrentScope(); - const client = scope.getClient() || getClient(); if (this._sampled !== true) { - // At this point if `sampled !== true` we want to discard the transaction. - DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); - - if (client) { - client.recordDroppedEvent('sample_rate', 'transaction'); - } - return undefined; } @@ -355,6 +345,14 @@ export class SentrySpan implements Span { const source = this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource | undefined; + // remove internal root span attributes we don't need to send. + /* eslint-disable @typescript-eslint/no-dynamic-delete */ + delete this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + spans.forEach(span => { + delete span.data[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + }); + // eslint-enabled-next-line @typescript-eslint/no-dynamic-delete + const transaction: TransactionEvent = { contexts: { trace: spanToTransactionTraceContext(this), @@ -376,7 +374,6 @@ export class SentrySpan implements Span { dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), }), }, - _metrics_summary: getMetricSummaryJsonForSpan(this), ...(source && { transaction_info: { source, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index d44d0b216db3..64a4c8ba5a4c 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -2,18 +2,27 @@ import type { AsyncContextStrategy } from '../asyncContext/types'; import { getMainCarrier } from '../carrier'; -import type { ClientOptions, Scope, SentrySpanArguments, Span, SpanTimeInput, StartSpanOptions } from '../types-hoist'; +import type { + ClientOptions, + DynamicSamplingContext, + SentrySpanArguments, + Span, + SpanTimeInput, + StartSpanOptions, +} from '../types-hoist'; import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes'; import { getAsyncContextStrategy } from '../asyncContext'; import { DEBUG_BUILD } from '../debug-build'; +import type { Scope } from '../scope'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; import { logger } from '../utils-hoist/logger'; import { generateTraceId } from '../utils-hoist/propagationContext'; import { propagationContextFromHeaders } from '../utils-hoist/tracing'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; +import { parseSampleRate } from '../utils/parseSampleRate'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; @@ -43,9 +52,13 @@ export function startSpan(options: StartSpanOptions, callback: (span: Span) = } const spanArguments = parseSentrySpanArguments(options); - const { forceTransaction, parentSpan: customParentSpan } = options; + const { forceTransaction, parentSpan: customParentSpan, scope: customScope } = options; + + // We still need to fork a potentially passed scope, as we set the active span on it + // and we need to ensure that it is cleaned up properly once the span ends. + const customForkedScope = customScope?.clone(); - return withScope(options.scope, () => { + return withScope(customForkedScope, () => { // If `options.parentSpan` is defined, we want to wrap the callback in `withActiveSpan` const wrapper = getActiveSpanWrapper(customParentSpan); @@ -74,7 +87,9 @@ export function startSpan(options: StartSpanOptions, callback: (span: Span) = activeSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); } }, - () => activeSpan.end(), + () => { + activeSpan.end(); + }, ); }); }); @@ -82,7 +97,7 @@ export function startSpan(options: StartSpanOptions, callback: (span: Span) = /** * Similar to `Sentry.startSpan`. Wraps a function with a transaction/span, but does not finish the span - * after the function is done automatically. You'll have to call `span.end()` manually. + * after the function is done automatically. Use `span.end()` to end the span. * * The created span is the active span and will be used as parent by other spans created inside the function * and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active. @@ -97,9 +112,11 @@ export function startSpanManual(options: StartSpanOptions, callback: (span: S } const spanArguments = parseSentrySpanArguments(options); - const { forceTransaction, parentSpan: customParentSpan } = options; + const { forceTransaction, parentSpan: customParentSpan, scope: customScope } = options; - return withScope(options.scope, () => { + const customForkedScope = customScope?.clone(); + + return withScope(customForkedScope, () => { // If `options.parentSpan` is defined, we want to wrap the callback in `withActiveSpan` const wrapper = getActiveSpanWrapper(customParentSpan); @@ -119,12 +136,12 @@ export function startSpanManual(options: StartSpanOptions, callback: (span: S _setSpanForScope(scope, activeSpan); - function finishAndSetSpan(): void { - activeSpan.end(); - } - return handleCallbackErrors( - () => callback(activeSpan, finishAndSetSpan), + // We pass the `finish` function to the callback, so the user can finish the span manually + // this is mainly here for historic purposes because previously, we instructed users to call + // `finish` instead of `span.end()` to also clean up the scope. Nowadays, calling `span.end()` + // or `finish` has the same effect and we simply leave it here to avoid breaking user code. + () => callback(activeSpan, () => activeSpan.end()), () => { // Only update the span status if it hasn't been changed yet, and the span is not yet finished const { status } = spanToJSON(activeSpan); @@ -191,15 +208,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span { * be attached to the incoming trace. */ export const continueTrace = ( - { - sentryTrace, - baggage, - }: { + options: { sentryTrace: Parameters[0]; baggage: Parameters[1]; }, callback: () => V, ): V => { + const carrier = getMainCarrier(); + const acs = getAsyncContextStrategy(carrier); + if (acs.continueTrace) { + return acs.continueTrace(options, callback); + } + + const { sentryTrace, baggage } = options; + return withScope(scope => { const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); scope.setPropagationContext(propagationContext); @@ -260,7 +282,10 @@ export function suppressTracing(callback: () => T): T { */ export function startNewTrace(callback: () => T): T { return withScope(scope => { - scope.setPropagationContext({ traceId: generateTraceId() }); + scope.setPropagationContext({ + traceId: generateTraceId(), + sampleRand: Math.random(), + }); DEBUG_BUILD && logger.info(`Starting a new trace with id ${scope.getPropagationContext().traceId}`); return withActiveSpan(null, callback); }); @@ -278,7 +303,21 @@ function createChildOrRootSpan({ scope: Scope; }): Span { if (!hasTracingEnabled()) { - return new SentryNonRecordingSpan(); + const span = new SentryNonRecordingSpan(); + + // If this is a root span, we ensure to freeze a DSC + // So we can have at least partial data here + if (forceTransaction || !parentSpan) { + const dsc = { + sampled: 'false', + sample_rate: '0', + transaction: spanArguments.name, + ...getDynamicSamplingContextFromSpan(span), + } satisfies Partial; + freezeDscOnSpan(span, dsc); + } + + return span; } const isolationScope = getIsolationScope(); @@ -366,31 +405,39 @@ function getAcs(): AsyncContextStrategy { function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parentSampled?: boolean): SentrySpan { const client = getClient(); - const options: Partial = (client && client.getOptions()) || {}; + const options: Partial = client?.getOptions() || {}; const { name = '', attributes } = spanArguments; - const [sampled, sampleRate] = scope.getScopeData().sdkProcessingMetadata[SUPPRESS_TRACING_KEY] + const currentPropagationContext = scope.getPropagationContext(); + const [sampled, sampleRate, localSampleRateWasApplied] = scope.getScopeData().sdkProcessingMetadata[ + SUPPRESS_TRACING_KEY + ] ? [false] - : sampleSpan(options, { - name, - parentSampled, - attributes, - transactionContext: { + : sampleSpan( + options, + { name, parentSampled, + attributes, + parentSampleRate: parseSampleRate(currentPropagationContext.dsc?.sample_rate), }, - }); + currentPropagationContext.sampleRand, + ); const rootSpan = new SentrySpan({ ...spanArguments, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: + sampleRate !== undefined && localSampleRateWasApplied ? sampleRate : undefined, ...spanArguments.attributes, }, sampled, }); - if (sampleRate !== undefined) { - rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate); + + if (!sampled && client) { + DEBUG_BUILD && logger.log('[Tracing] Discarding root span because its trace was not chosen to be sampled.'); + client.recordDroppedEvent('sample_rate', 'transaction'); } if (client) { diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index a0442aa0eeec..61e2dcce2bc1 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -1,5 +1,5 @@ +import type { Scope } from '../scope'; import type { Span } from '../types-hoist'; -import type { Scope } from '../types-hoist'; import { addNonEnumerableProperty } from '../utils-hoist/object'; const SCOPE_ON_START_SPAN_FIELD = '_sentryScope'; diff --git a/packages/core/src/transports/base.ts b/packages/core/src/transports/base.ts index 5303e43e6adf..9296095428cf 100644 --- a/packages/core/src/transports/base.ts +++ b/packages/core/src/transports/base.ts @@ -1,17 +1,13 @@ +import { DEBUG_BUILD } from '../debug-build'; import type { Envelope, EnvelopeItem, - EnvelopeItemType, - Event, EventDropReason, - EventItem, InternalBaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequestExecutor, } from '../types-hoist'; - -import { DEBUG_BUILD } from '../debug-build'; import { createEnvelope, envelopeItemTypeToDataCategory, @@ -49,8 +45,7 @@ export function createTransport( forEachEnvelopeItem(envelope, (item, type) => { const dataCategory = envelopeItemTypeToDataCategory(type); if (isRateLimited(rateLimits, dataCategory)) { - const event: Event | undefined = getEventForEnvelopeItem(item, type); - options.recordDroppedEvent('ratelimit_backoff', dataCategory, event); + options.recordDroppedEvent('ratelimit_backoff', dataCategory); } else { filteredEnvelopeItems.push(item); } @@ -66,8 +61,7 @@ export function createTransport( // Creates client report for each item in an envelope const recordEnvelopeLoss = (reason: EventDropReason): void => { forEachEnvelopeItem(filteredEnvelope, (item, type) => { - const event: Event | undefined = getEventForEnvelopeItem(item, type); - options.recordDroppedEvent(reason, envelopeItemTypeToDataCategory(type), event); + options.recordDroppedEvent(reason, envelopeItemTypeToDataCategory(type)); }); }; @@ -107,11 +101,3 @@ export function createTransport( flush, }; } - -function getEventForEnvelopeItem(item: Envelope[1][number], type: EnvelopeItemType): Event | undefined { - if (type !== 'event' && type !== 'transaction') { - return undefined; - } - - return Array.isArray(item) ? (item as EventItem)[1] : undefined; -} diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index 29534fd13d9b..16d4da1cf859 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -121,7 +121,7 @@ export function makeMultiplexedTransport( async function send(envelope: Envelope): Promise { function getEvent(types?: EnvelopeItemType[]): Event | undefined { - const eventTypes: EnvelopeItemType[] = types && types.length ? types : ['event']; + const eventTypes: EnvelopeItemType[] = types?.length ? types : ['event']; return eventFromEnvelope(envelope, eventTypes); } diff --git a/packages/core/src/transports/offline.ts b/packages/core/src/transports/offline.ts index cf8902739db7..0b99baba1e4b 100644 --- a/packages/core/src/transports/offline.ts +++ b/packages/core/src/transports/offline.ts @@ -134,9 +134,9 @@ export function makeOfflineTransport( if (result) { // If there's a retry-after header, use that as the next delay. - if (result.headers && result.headers['retry-after']) { + if (result.headers?.['retry-after']) { delay = parseRetryAfterHeader(result.headers['retry-after']); - } else if (result.headers && result.headers['x-sentry-rate-limits']) { + } else if (result.headers?.['x-sentry-rate-limits']) { delay = 60_000; // 60 seconds } // If we have a server error, return now so we don't flush the queue. else if ((result.statusCode || 0) >= 400) { @@ -170,7 +170,15 @@ export function makeOfflineTransport( return { send, - flush: t => transport.flush(t), + flush: timeout => { + // If there's no timeout, we should attempt to flush the offline queue. + if (timeout === undefined) { + retryDelay = START_DELAY; + flushIn(MIN_DELAY); + } + + return transport.flush(timeout); + }, }; }; } diff --git a/packages/core/src/trpc.ts b/packages/core/src/trpc.ts index eddabd123250..e9b4f733078a 100644 --- a/packages/core/src/trpc.ts +++ b/packages/core/src/trpc.ts @@ -44,14 +44,14 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { const { path, type, next, rawInput, getRawInput } = opts; const client = getClient(); - const clientOptions = client && client.getOptions(); + const clientOptions = client?.getOptions(); const trpcContext: Record = { procedure_path: path, procedure_type: type, }; - if (options.attachRpcInput !== undefined ? options.attachRpcInput : clientOptions && clientOptions.sendDefaultPii) { + if (options.attachRpcInput !== undefined ? options.attachRpcInput : clientOptions?.sendDefaultPii) { if (rawInput !== undefined) { trpcContext.input = normalize(rawInput); } diff --git a/packages/core/src/types-hoist/client.ts b/packages/core/src/types-hoist/client.ts deleted file mode 100644 index 06e3109e1e14..000000000000 --- a/packages/core/src/types-hoist/client.ts +++ /dev/null @@ -1,401 +0,0 @@ -import type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; -import type { CheckIn, MonitorConfig } from './checkin'; -import type { EventDropReason } from './clientreport'; -import type { DataCategory } from './datacategory'; -import type { DsnComponents } from './dsn'; -import type { DynamicSamplingContext, Envelope } from './envelope'; -import type { Event, EventHint } from './event'; -import type { EventProcessor } from './eventprocessor'; -import type { FeedbackEvent } from './feedback'; -import type { Integration } from './integration'; -import type { ClientOptions } from './options'; -import type { ParameterizedString } from './parameterize'; -import type { Scope } from './scope'; -import type { SdkMetadata } from './sdkmetadata'; -import type { Session, SessionAggregates } from './session'; -import type { SeverityLevel } from './severity'; -import type { Span, SpanAttributes, SpanContextData } from './span'; -import type { StartSpanOptions } from './startSpanOptions'; -import type { Transport, TransportMakeRequestResponse } from './transport'; - -/** - * User-Facing Sentry SDK Client. - * - * This interface contains all methods to interface with the SDK once it has - * been installed. It allows to send events to Sentry, record breadcrumbs and - * set a context included in every event. Since the SDK mutates its environment, - * there will only be one instance during runtime. - * - */ -export interface Client { - /** - * Captures an exception event and sends it to Sentry. - * - * Unlike `captureException` exported from every SDK, this method requires that you pass it the current scope. - * - * @param exception An exception-like object. - * @param hint May contain additional information about the original exception. - * @param currentScope An optional scope containing event metadata. - * @returns The event id - */ - captureException(exception: any, hint?: EventHint, currentScope?: Scope): string; - - /** - * Captures a message event and sends it to Sentry. - * - * Unlike `captureMessage` exported from every SDK, this method requires that you pass it the current scope. - * - * @param message The message to send to Sentry. - * @param level Define the level of the message. - * @param hint May contain additional information about the original exception. - * @param currentScope An optional scope containing event metadata. - * @returns The event id - */ - captureMessage(message: string, level?: SeverityLevel, hint?: EventHint, currentScope?: Scope): string; - - /** - * Captures a manually created event and sends it to Sentry. - * - * Unlike `captureEvent` exported from every SDK, this method requires that you pass it the current scope. - * - * @param event The event to send to Sentry. - * @param hint May contain additional information about the original exception. - * @param currentScope An optional scope containing event metadata. - * @returns The event id - */ - captureEvent(event: Event, hint?: EventHint, currentScope?: Scope): string; - - /** - * Captures a session - * - * @param session Session to be delivered - */ - captureSession(session: Session): void; - - /** - * Create a cron monitor check in and send it to Sentry. This method is not available on all clients. - * - * @param checkIn An object that describes a check in. - * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want - * to create a monitor automatically when sending a check in. - * @param scope An optional scope containing event metadata. - * @returns A string representing the id of the check in. - */ - captureCheckIn?(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string; - - /** Returns the current Dsn. */ - getDsn(): DsnComponents | undefined; - - /** Returns the current options. */ - getOptions(): O; - - /** - * @inheritdoc - * - */ - getSdkMetadata(): SdkMetadata | undefined; - - /** - * Returns the transport that is used by the client. - * Please note that the transport gets lazy initialized so it will only be there once the first event has been sent. - * - * @returns The transport. - */ - getTransport(): Transport | undefined; - - /** - * Flush the event queue and set the client to `enabled = false`. See {@link Client.flush}. - * - * @param timeout Maximum time in ms the client should wait before shutting down. Omitting this parameter will cause - * the client to wait until all events are sent before disabling itself. - * @returns A promise which resolves to `true` if the flush completes successfully before the timeout, or `false` if - * it doesn't. - */ - close(timeout?: number): PromiseLike; - - /** - * Wait for all events to be sent or the timeout to expire, whichever comes first. - * - * @param timeout Maximum time in ms the client should wait for events to be flushed. Omitting this parameter will - * cause the client to wait until all events are sent before resolving the promise. - * @returns A promise that will resolve with `true` if all events are sent before the timeout, or `false` if there are - * still events in the queue when the timeout is reached. - */ - flush(timeout?: number): PromiseLike; - - /** - * Adds an event processor that applies to any event processed by this client. - */ - addEventProcessor(eventProcessor: EventProcessor): void; - - /** - * Get all added event processors for this client. - */ - getEventProcessors(): EventProcessor[]; - - /** Get the instance of the integration with the given name on the client, if it was added. */ - getIntegrationByName(name: string): T | undefined; - - /** - * Add an integration to the client. - * This can be used to e.g. lazy load integrations. - * In most cases, this should not be necessary, and you're better off just passing the integrations via `integrations: []` at initialization time. - * However, if you find the need to conditionally load & add an integration, you can use `addIntegration` to do so. - * - * */ - addIntegration(integration: Integration): void; - - /** - * Initialize this client. - * Call this after the client was set on a scope. - */ - init(): void; - - /** Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`. */ - eventFromException(exception: any, hint?: EventHint): PromiseLike; - - /** Creates an {@link Event} from primitive inputs to `captureMessage`. */ - eventFromMessage(message: ParameterizedString, level?: SeverityLevel, hint?: EventHint): PromiseLike; - - /** Submits the event to Sentry */ - sendEvent(event: Event, hint?: EventHint): void; - - /** Submits the session to Sentry */ - sendSession(session: Session | SessionAggregates): void; - - /** Sends an envelope to Sentry */ - sendEnvelope(envelope: Envelope): PromiseLike; - - /** - * Record on the client that an event got dropped (ie, an event that will not be sent to sentry). - * - * @param reason The reason why the event got dropped. - * @param category The data category of the dropped event. - * @param event The dropped event. - */ - recordDroppedEvent(reason: EventDropReason, dataCategory: DataCategory, event?: Event): void; - - // HOOKS - /* eslint-disable @typescript-eslint/unified-signatures */ - - /** - * Register a callback for whenever a span is started. - * Receives the span as argument. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'spanStart', callback: (span: Span) => void): () => void; - - /** - * Register a callback before span sampling runs. Receives a `samplingDecision` object argument with a `decision` - * property that can be used to make a sampling decision that will be enforced, before any span sampling runs. - * @returns A function that, when executed, removes the registered callback. - */ - on( - hook: 'beforeSampling', - callback: ( - samplingData: { - spanAttributes: SpanAttributes; - spanName: string; - parentSampled?: boolean; - parentContext?: SpanContextData; - }, - samplingDecision: { decision: boolean }, - ) => void, - ): void; - - /** - * Register a callback for whenever a span is ended. - * Receives the span as argument. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'spanEnd', callback: (span: Span) => void): () => void; - - /** - * Register a callback for when an idle span is allowed to auto-finish. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): () => void; - - /** - * Register a callback for transaction start and finish. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): () => void; - - /** - * Register a callback that runs when stack frame metadata should be applied to an event. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'applyFrameMetadata', callback: (event: Event) => void): () => void; - - /** - * Register a callback for before sending an event. - * This is called right before an event is sent and should not be used to mutate the event. - * Receives an Event & EventHint as arguments. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint | undefined) => void): () => void; - - /** - * Register a callback for preprocessing an event, - * before it is passed to (global) event processors. - * Receives an Event & EventHint as arguments. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint | undefined) => void): () => void; - - /** - * Register a callback for when an event has been sent. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'afterSendEvent', callback: (event: Event, sendResponse: TransportMakeRequestResponse) => void): () => void; - - /** - * Register a callback before a breadcrumb is added. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): () => void; - - /** - * Register a callback when a DSC (Dynamic Sampling Context) is created. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext, rootSpan?: Span) => void): () => void; - - /** - * Register a callback when a Feedback event has been prepared. - * This should be used to mutate the event. The options argument can hint - * about what kind of mutation it expects. - * @returns A function that, when executed, removes the registered callback. - */ - on( - hook: 'beforeSendFeedback', - callback: (feedback: FeedbackEvent, options?: { includeReplay?: boolean }) => void, - ): () => void; - - /** - * A hook for the browser tracing integrations to trigger a span start for a page load. - * @returns A function that, when executed, removes the registered callback. - */ - on( - hook: 'startPageLoadSpan', - callback: ( - options: StartSpanOptions, - traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined }, - ) => void, - ): () => void; - - /** - * A hook for browser tracing integrations to trigger a span for a navigation. - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): () => void; - - /** - * A hook that is called when the client is flushing - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'flush', callback: () => void): () => void; - - /** - * A hook that is called when the client is closing - * @returns A function that, when executed, removes the registered callback. - */ - on(hook: 'close', callback: () => void): () => void; - - /** Fire a hook whenever a span starts. */ - emit(hook: 'spanStart', span: Span): void; - - /** A hook that is called every time before a span is sampled. */ - emit( - hook: 'beforeSampling', - samplingData: { - spanAttributes: SpanAttributes; - spanName: string; - parentSampled?: boolean; - parentContext?: SpanContextData; - }, - samplingDecision: { decision: boolean }, - ): void; - - /** Fire a hook whenever a span ends. */ - emit(hook: 'spanEnd', span: Span): void; - - /** - * Fire a hook indicating that an idle span is allowed to auto finish. - */ - emit(hook: 'idleSpanEnableAutoFinish', span: Span): void; - - /* - * Fire a hook event for envelope creation and sending. Expects to be given an envelope as the - * second argument. - */ - emit(hook: 'beforeEnvelope', envelope: Envelope): void; - - /* - * Fire a hook indicating that stack frame metadata should be applied to the event passed to the hook. - */ - emit(hook: 'applyFrameMetadata', event: Event): void; - - /** - * Fire a hook event before sending an event. - * This is called right before an event is sent and should not be used to mutate the event. - * Expects to be given an Event & EventHint as the second/third argument. - */ - emit(hook: 'beforeSendEvent', event: Event, hint?: EventHint): void; - - /** - * Fire a hook event to process events before they are passed to (global) event processors. - * Expects to be given an Event & EventHint as the second/third argument. - */ - emit(hook: 'preprocessEvent', event: Event, hint?: EventHint): void; - - /* - * Fire a hook event after sending an event. Expects to be given an Event as the - * second argument. - */ - emit(hook: 'afterSendEvent', event: Event, sendResponse: TransportMakeRequestResponse): void; - - /** - * Fire a hook for when a breadcrumb is added. Expects the breadcrumb as second argument. - */ - emit(hook: 'beforeAddBreadcrumb', breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void; - - /** - * Fire a hook for when a DSC (Dynamic Sampling Context) is created. Expects the DSC as second argument. - */ - emit(hook: 'createDsc', dsc: DynamicSamplingContext, rootSpan?: Span): void; - - /** - * Fire a hook event for after preparing a feedback event. Events to be given - * a feedback event as the second argument, and an optional options object as - * third argument. - */ - emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay?: boolean }): void; - - /** - * Emit a hook event for browser tracing integrations to trigger a span start for a page load. - */ - emit( - hook: 'startPageLoadSpan', - options: StartSpanOptions, - traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined }, - ): void; - - /** - * Emit a hook event for browser tracing integrations to trigger a span for a navigation. - */ - emit(hook: 'startNavigationSpan', options: StartSpanOptions): void; - - /** - * Emit a hook event for client flush - */ - emit(hook: 'flush'): void; - - /** - * Emit a hook event for client close - */ - emit(hook: 'close'): void; - - /* eslint-enable @typescript-eslint/unified-signatures */ -} diff --git a/packages/core/src/types-hoist/datacategory.ts b/packages/core/src/types-hoist/datacategory.ts index bd1c0b693e4d..da90cc0ca90b 100644 --- a/packages/core/src/types-hoist/datacategory.ts +++ b/packages/core/src/types-hoist/datacategory.ts @@ -26,8 +26,6 @@ export type DataCategory = | 'monitor' // Feedback type event (v2) | 'feedback' - // Metrics sent via the statsd or metrics envelope items - | 'metric_bucket' // Span | 'span' // Unknown data category diff --git a/packages/core/src/types-hoist/envelope.ts b/packages/core/src/types-hoist/envelope.ts index b5e2599942d4..5a54ffc7b8c2 100644 --- a/packages/core/src/types-hoist/envelope.ts +++ b/packages/core/src/types-hoist/envelope.ts @@ -23,6 +23,7 @@ export type DynamicSamplingContext = { transaction?: string; replay_id?: string; sampled?: string; + sample_rand?: string; }; // https://github.com/getsentry/relay/blob/311b237cd4471042352fa45e7a0824b8995f216f/relay-server/src/envelope.rs#L154 @@ -41,7 +42,6 @@ export type EnvelopeItemType = | 'replay_event' | 'replay_recording' | 'check_in' - | 'statsd' | 'span' | 'raw_security'; @@ -84,7 +84,6 @@ type ReplayRecordingItemHeaders = { type: 'replay_recording'; length: number }; type CheckInItemHeaders = { type: 'check_in' }; type ProfileItemHeaders = { type: 'profile' }; type ProfileChunkItemHeaders = { type: 'profile_chunk' }; -type StatsdItemHeaders = { type: 'statsd'; length: number }; type SpanItemHeaders = { type: 'span' }; type RawSecurityHeaders = { type: 'raw_security'; sentry_release?: string; sentry_environment?: string }; @@ -98,7 +97,6 @@ export type ClientReportItem = BaseEnvelopeItem; type ReplayEventItem = BaseEnvelopeItem; type ReplayRecordingItem = BaseEnvelopeItem; -export type StatsdItem = BaseEnvelopeItem; export type FeedbackItem = BaseEnvelopeItem; export type ProfileItem = BaseEnvelopeItem; export type ProfileChunkItem = BaseEnvelopeItem; @@ -110,7 +108,6 @@ type SessionEnvelopeHeaders = { sent_at: string }; type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext }; type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders; type ReplayEnvelopeHeaders = BaseEnvelopeHeaders; -type StatsdEnvelopeHeaders = BaseEnvelopeHeaders; type SpanEnvelopeHeaders = BaseEnvelopeHeaders & { trace?: DynamicSamplingContext }; export type EventEnvelope = BaseEnvelope< @@ -121,7 +118,6 @@ export type SessionEnvelope = BaseEnvelope; export type ClientReportEnvelope = BaseEnvelope; export type ReplayEnvelope = [ReplayEnvelopeHeaders, [ReplayEventItem, ReplayRecordingItem]]; export type CheckInEnvelope = BaseEnvelope; -export type StatsdEnvelope = BaseEnvelope; export type SpanEnvelope = BaseEnvelope; export type ProfileChunkEnvelope = BaseEnvelope; export type RawSecurityEnvelope = BaseEnvelope; @@ -133,7 +129,6 @@ export type Envelope = | ProfileChunkEnvelope | ReplayEnvelope | CheckInEnvelope - | StatsdEnvelope | SpanEnvelope | RawSecurityEnvelope; diff --git a/packages/core/src/types-hoist/event.ts b/packages/core/src/types-hoist/event.ts index ecfa5ad14559..5b4d87337236 100644 --- a/packages/core/src/types-hoist/event.ts +++ b/packages/core/src/types-hoist/event.ts @@ -1,19 +1,17 @@ +import type { CaptureContext, SdkProcessingMetadata } from '../scope'; import type { Attachment } from './attachment'; import type { Breadcrumb } from './breadcrumb'; import type { Contexts } from './context'; import type { DebugMeta } from './debugMeta'; -import type { DynamicSamplingContext } from './envelope'; import type { Exception } from './exception'; import type { Extras } from './extra'; import type { Measurements } from './measurement'; import type { Mechanism } from './mechanism'; import type { Primitive } from './misc'; -import type { PolymorphicRequest } from './polymorphics'; import type { RequestEventData } from './request'; -import type { CaptureContext, Scope } from './scope'; import type { SdkInfo } from './sdkinfo'; import type { SeverityLevel } from './severity'; -import type { MetricSummary, SpanJSON } from './span'; +import type { SpanJSON } from './span'; import type { Thread } from './thread'; import type { TransactionSource } from './transaction'; import type { User } from './user'; @@ -54,14 +52,7 @@ export interface Event { debug_meta?: DebugMeta; // A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get sent to Sentry // Note: This is considered internal and is subject to change in minors - sdkProcessingMetadata?: { [key: string]: unknown } & { - request?: PolymorphicRequest; - normalizedRequest?: RequestEventData; - dynamicSamplingContext?: Partial; - capturedSpanScope?: Scope; - capturedSpanIsolationScope?: Scope; - spanCountBeforeProcessing?: number; - }; + sdkProcessingMetadata?: SdkProcessingMetadata; transaction_info?: { source: TransactionSource; }; @@ -82,7 +73,6 @@ export interface ErrorEvent extends Event { } export interface TransactionEvent extends Event { type: 'transaction'; - _metrics_summary?: Record>; } /** JSDoc */ diff --git a/packages/core/src/types-hoist/feedback/config.ts b/packages/core/src/types-hoist/feedback/config.ts index 4ec846c7d98d..d7b3d78995bb 100644 --- a/packages/core/src/types-hoist/feedback/config.ts +++ b/packages/core/src/types-hoist/feedback/config.ts @@ -57,6 +57,15 @@ export interface FeedbackGeneralConfiguration { name: string; }; + /** + * _experiments allows users to enable experimental or internal features. + * We don't consider such features as part of the public API and hence we don't guarantee semver for them. + * Experimental features can be added, changed or removed at any time. + * + * Default: undefined + */ + _experiments: Partial<{ annotations: boolean }>; + /** * Set an object that will be merged sent as tags data with the event. */ diff --git a/packages/core/src/types-hoist/hub.ts b/packages/core/src/types-hoist/hub.ts deleted file mode 100644 index 6fa109145146..000000000000 --- a/packages/core/src/types-hoist/hub.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; -import type { Client } from './client'; -import type { Event, EventHint } from './event'; -import type { Extra, Extras } from './extra'; -import type { Integration, IntegrationClass } from './integration'; -import type { Primitive } from './misc'; -import type { Scope } from './scope'; -import type { Session } from './session'; -import type { SeverityLevel } from './severity'; -import type { User } from './user'; - -/** - * Internal class used to make sure we always have the latest internal functions - * working in case we have a version conflict. - * - * @deprecated This interface will be removed in a future major version of the SDK in favour of - * `Scope` and `Client` objects and APIs. - * - * Most APIs referencing `Hub` are themselves and will be removed in version 8 of the SDK. More information: - * - [Migration Guide](https://github.com/getsentry/sentry-javascript/blob/develop/MIGRATION.md#deprecate-hub) - * - */ -export interface Hub { - /** - * This binds the given client to the current scope. - * @param client An SDK client (client) instance. - * - * @deprecated Use `initAndBind()` directly. - */ - bindClient(client?: Client): void; - - /** - * Creates a new scope with and executes the given operation within. - * The scope is automatically removed once the operation - * finishes or throws. - * - * This is essentially a convenience function for: - * - * pushScope(); - * callback(); - * popScope(); - * - * @param callback that will be enclosed into push/popScope. - * - * @deprecated Use `Sentry.withScope()` instead. - */ - withScope(callback: (scope: Scope) => T): T; - - /** - * Returns the client of the top stack. - * @deprecated Use `Sentry.getClient()` instead. - */ - getClient(): C | undefined; - - /** - * Returns the scope of the top stack. - * @deprecated Use `Sentry.getCurrentScope()` instead. - */ - getScope(): Scope; - - /** - * Get the currently active isolation scope. - * The isolation scope is used to isolate data between different hubs. - * - * @deprecated Use `Sentry.getIsolationScope()` instead. - */ - getIsolationScope(): Scope; - - /** - * Captures an exception event and sends it to Sentry. - * - * @param exception An exception-like object. - * @param hint May contain additional information about the original exception. - * @returns The generated eventId. - * - * @deprecated Use `Sentry.captureException()` instead. - */ - captureException(exception: any, hint?: EventHint): string; - - /** - * Captures a message event and sends it to Sentry. - * - * @param message The message to send to Sentry. - * @param level Define the level of the message. - * @param hint May contain additional information about the original exception. - * @returns The generated eventId. - * - * @deprecated Use `Sentry.captureMessage()` instead. - */ - captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string; - - /** - * Captures a manually created event and sends it to Sentry. - * - * @param event The event to send to Sentry. - * @param hint May contain additional information about the original exception. - * - * @deprecated Use `Sentry.captureEvent()` instead. - */ - captureEvent(event: Event, hint?: EventHint): string; - - /** - * Records a new breadcrumb which will be attached to future events. - * - * Breadcrumbs will be added to subsequent events to provide more context on - * user's actions prior to an error or crash. - * - * @param breadcrumb The breadcrumb to record. - * @param hint May contain additional information about the original breadcrumb. - * - * @deprecated Use `Sentry.addBreadcrumb()` instead. - */ - addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void; - - /** - * Updates user context information for future events. - * - * @param user User context object to be set in the current context. Pass `null` to unset the user. - * - * @deprecated Use `Sentry.setUser()` instead. - */ - setUser(user: User | null): void; - - /** - * Set an object that will be merged sent as tags data with the event. - * - * @param tags Tags context object to merge into current context. - * - * @deprecated Use `Sentry.setTags()` instead. - */ - setTags(tags: { [key: string]: Primitive }): void; - - /** - * Set key:value that will be sent as tags data with the event. - * - * Can also be used to unset a tag, by passing `undefined`. - * - * @param key String key of tag - * @param value Value of tag - * - * @deprecated Use `Sentry.setTag()` instead. - */ - setTag(key: string, value: Primitive): void; - - /** - * Set key:value that will be sent as extra data with the event. - * @param key String of extra - * @param extra Any kind of data. This data will be normalized. - * - * @deprecated Use `Sentry.setExtra()` instead. - */ - setExtra(key: string, extra: Extra): void; - - /** - * Set an object that will be merged sent as extra data with the event. - * @param extras Extras object to merge into current context. - * - * @deprecated Use `Sentry.setExtras()` instead. - */ - setExtras(extras: Extras): void; - - /** - * Sets context data with the given name. - * @param name of the context - * @param context Any kind of data. This data will be normalized. - * - * @deprecated Use `Sentry.setContext()` instead. - */ - setContext(name: string, context: { [key: string]: any } | null): void; - - /** - * Returns the integration if installed on the current client. - * - * @deprecated Use `Sentry.getClient().getIntegration()` instead. - */ - getIntegration(integration: IntegrationClass): T | null; - - /** - * Starts a new `Session`, sets on the current scope and returns it. - * - * To finish a `session`, it has to be passed directly to `client.captureSession`, which is done automatically - * when using `hub.endSession()` for the session currently stored on the scope. - * - * When there's already an existing session on the scope, it'll be automatically ended. - * - * @param context Optional properties of the new `Session`. - * - * @returns The session which was just started - * - * @deprecated Use top-level `startSession` instead. - */ - startSession(context?: Session): Session; - - /** - * Ends the session that lives on the current scope and sends it to Sentry - * - * @deprecated Use top-level `endSession` instead. - */ - endSession(): void; - - /** - * Sends the current session on the scope to Sentry - * - * @param endSession If set the session will be marked as exited and removed from the scope - * - * @deprecated Use top-level `captureSession` instead. - */ - captureSession(endSession?: boolean): void; -} diff --git a/packages/core/src/types-hoist/index.ts b/packages/core/src/types-hoist/index.ts index 3433c17092cf..c1cbe5284808 100644 --- a/packages/core/src/types-hoist/index.ts +++ b/packages/core/src/types-hoist/index.ts @@ -7,7 +7,6 @@ export type { FetchBreadcrumbHint, XhrBreadcrumbHint, } from './breadcrumb'; -export type { Client } from './client'; export type { ClientReport, Outcome, EventDropReason } from './clientreport'; export type { Context, @@ -45,8 +44,6 @@ export type { CheckInEnvelope, RawSecurityEnvelope, RawSecurityItem, - StatsdItem, - StatsdEnvelope, ProfileItem, ProfileChunkEnvelope, ProfileChunkItem, @@ -58,9 +55,7 @@ export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from ' export type { EventProcessor } from './eventprocessor'; export type { Exception } from './exception'; export type { Extra, Extras } from './extra'; -// eslint-disable-next-line deprecation/deprecation -export type { Hub } from './hub'; -export type { Integration, IntegrationClass, IntegrationFn } from './integration'; +export type { Integration, IntegrationFn } from './integration'; export type { Mechanism } from './mechanism'; export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './misc'; export type { ClientOptions, Options } from './options'; @@ -92,12 +87,9 @@ export type { export type { QueryParams, RequestEventData, - // eslint-disable-next-line deprecation/deprecation - Request, SanitizedRequestData, } from './request'; export type { Runtime } from './runtime'; -export type { CaptureContext, Scope, ScopeContext, ScopeData } from './scope'; export type { SdkInfo } from './sdkinfo'; export type { SdkMetadata } from './sdkmetadata'; export type { @@ -106,12 +98,6 @@ export type { Session, SessionContext, SessionStatus, - // eslint-disable-next-line deprecation/deprecation - RequestSession, - // eslint-disable-next-line deprecation/deprecation - RequestSessionStatus, - // eslint-disable-next-line deprecation/deprecation - SessionFlusherLike, SerializedSession, } from './session'; @@ -126,7 +112,6 @@ export type { SpanJSON, SpanContextData, TraceFlag, - MetricSummary, } from './span'; export type { SpanStatus } from './spanStatus'; export type { TimedEvent } from './timedEvent'; @@ -174,13 +159,6 @@ export type { export type { BrowserClientReplayOptions, BrowserClientProfilingOptions } from './browseroptions'; export type { CheckIn, MonitorConfig, FinishedCheckIn, InProgressCheckIn, SerializedCheckIn } from './checkin'; -export type { - MetricsAggregator, - MetricBucketItem, - MetricInstance, - MetricData, - Metrics, -} from './metrics'; export type { ParameterizedString } from './parameterize'; export type { ContinuousProfiler, ProfilingIntegration, Profiler } from './profiling'; export type { ViewHierarchyData, ViewHierarchyWindow } from './view-hierarchy'; diff --git a/packages/core/src/types-hoist/integration.ts b/packages/core/src/types-hoist/integration.ts index deb23baaca51..cc9e4bc580ce 100644 --- a/packages/core/src/types-hoist/integration.ts +++ b/packages/core/src/types-hoist/integration.ts @@ -1,16 +1,6 @@ -import type { Client } from './client'; +import type { Client } from '../client'; import type { Event, EventHint } from './event'; -/** Integration Class Interface */ -export interface IntegrationClass { - /** - * Property that holds the integration name - */ - id: string; - - new (...args: any[]): T; -} - /** Integration interface */ export interface Integration { /** diff --git a/packages/core/src/types-hoist/metrics.ts b/packages/core/src/types-hoist/metrics.ts deleted file mode 100644 index 474f5b94c207..000000000000 --- a/packages/core/src/types-hoist/metrics.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { Client } from './client'; -import type { DurationUnit, MeasurementUnit } from './measurement'; -import type { Primitive } from './misc'; - -export interface MetricData { - unit?: MeasurementUnit; - tags?: Record; - timestamp?: number; - client?: Client; -} - -/** - * An abstract definition of the minimum required API - * for a metric instance. - */ -export interface MetricInstance { - /** - * Returns the weight of the metric. - */ - weight: number; - - /** - * Adds a value to a metric. - */ - add(value: number | string): void; - - /** - * Serializes the metric into a statsd format string. - */ - toString(): string; -} - -export interface MetricBucketItem { - metric: MetricInstance; - timestamp: number; - metricType: 'c' | 'g' | 's' | 'd'; - name: string; - unit: MeasurementUnit; - tags: Record; -} - -/** - * A metrics aggregator that aggregates metrics in memory and flushes them periodically. - */ -export interface MetricsAggregator { - /** - * Add a metric to the aggregator. - */ - add( - metricType: 'c' | 'g' | 's' | 'd', - name: string, - value: number | string, - unit?: MeasurementUnit, - tags?: Record, - timestamp?: number, - ): void; - - /** - * Flushes the current metrics to the transport via the transport. - */ - flush(): void; - - /** - * Shuts down metrics aggregator and clears all metrics. - */ - close(): void; - - /** - * Returns a string representation of the aggregator. - */ - toString(): string; -} - -export interface Metrics { - /** - * Adds a value to a counter metric - * - * @experimental This API is experimental and might have breaking changes in the future. - */ - increment(name: string, value?: number, data?: MetricData): void; - - /** - * Adds a value to a distribution metric - * - * @experimental This API is experimental and might have breaking changes in the future. - */ - distribution(name: string, value: number, data?: MetricData): void; - - /** - * Adds a value to a set metric. Value must be a string or integer. - * - * @experimental This API is experimental and might have breaking changes in the future. - */ - set(name: string, value: number | string, data?: MetricData): void; - - /** - * Adds a value to a gauge metric - * - * @experimental This API is experimental and might have breaking changes in the future. - */ - gauge(name: string, value: number, data?: MetricData): void; - - /** - * Adds a timing metric. - * The metric is added as a distribution metric. - * - * You can either directly capture a numeric `value`, or wrap a callback function in `timing`. - * In the latter case, the duration of the callback execution will be captured as a span & a metric. - * - * @experimental This API is experimental and might have breaking changes in the future. - */ - timing(name: string, value: number, unit?: DurationUnit, data?: Omit): void; - timing(name: string, callback: () => T, unit?: DurationUnit, data?: Omit): T; -} diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 38748df82fd5..49f3daa93b8e 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -1,8 +1,8 @@ +import type { CaptureContext } from '../scope'; import type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; import type { ErrorEvent, EventHint, TransactionEvent } from './event'; import type { Integration } from './integration'; import type { SamplingContext } from './samplingcontext'; -import type { CaptureContext } from './scope'; import type { SdkMetadata } from './sdkmetadata'; import type { SpanJSON } from './span'; import type { StackLineParser, StackParser } from './stacktrace'; @@ -24,16 +24,6 @@ export interface ClientOptions SpanJSON | null; + beforeSendSpan?: (span: SpanJSON) => SpanJSON; /** * An event-processing callback for transaction events, guaranteed to be invoked after all other event diff --git a/packages/core/src/types-hoist/profiling.ts b/packages/core/src/types-hoist/profiling.ts index 9ecba8ced48a..7f4f316a9d0b 100644 --- a/packages/core/src/types-hoist/profiling.ts +++ b/packages/core/src/types-hoist/profiling.ts @@ -1,4 +1,4 @@ -import type { Client } from './client'; +import type { Client } from '../client'; import type { DebugImage } from './debugMeta'; import type { Integration } from './integration'; import type { MeasurementUnit } from './measurement'; diff --git a/packages/core/src/types-hoist/request.ts b/packages/core/src/types-hoist/request.ts index 6ba060219dfd..834249cdd24e 100644 --- a/packages/core/src/types-hoist/request.ts +++ b/packages/core/src/types-hoist/request.ts @@ -4,19 +4,13 @@ export interface RequestEventData { url?: string; method?: string; - data?: any; + data?: unknown; query_string?: QueryParams; - cookies?: { [key: string]: string }; - env?: { [key: string]: string }; + cookies?: Record; + env?: Record; headers?: { [key: string]: string }; } -/** - * Request data included in an event as sent to Sentry. - * @deprecated: This type will be removed in v9. Use `RequestEventData` instead. - */ -export type Request = RequestEventData; - export type QueryParams = string | { [key: string]: string } | Array<[string, string]>; /** diff --git a/packages/core/src/types-hoist/samplingcontext.ts b/packages/core/src/types-hoist/samplingcontext.ts index ecce87d7fbc7..6f0d2a0800cf 100644 --- a/packages/core/src/types-hoist/samplingcontext.ts +++ b/packages/core/src/types-hoist/samplingcontext.ts @@ -1,4 +1,5 @@ -import type { ExtractedNodeRequestData, WorkerLocation } from './misc'; +import type { RequestEventData } from '../types-hoist/request'; +import type { WorkerLocation } from './misc'; import type { SpanAttributes } from './span'; /** @@ -11,22 +12,18 @@ export interface CustomSamplingContext { /** * Data passed to the `tracesSampler` function, which forms the basis for whatever decisions it might make. * - * Adds default data to data provided by the user. See {@link Hub.startTransaction} + * Adds default data to data provided by the user. */ export interface SamplingContext extends CustomSamplingContext { /** - * Context data with which transaction being sampled was created. - * @deprecated This is duplicate data and will be removed eventually. + * Sampling decision from the parent transaction, if any. */ - transactionContext: { - name: string; - parentSampled?: boolean | undefined; - }; + parentSampled?: boolean; /** - * Sampling decision from the parent transaction, if any. + * Sample rate that is coming from an incoming trace (if there is one). */ - parentSampled?: boolean; + parentSampleRate?: number; /** * Object representing the URL of the current page or worker script. Passed by default when using the `BrowserTracing` @@ -35,9 +32,9 @@ export interface SamplingContext extends CustomSamplingContext { location?: WorkerLocation; /** - * Object representing the incoming request to a node server. Passed by default when using the TracingHandler. + * Object representing the incoming request to a node server in a normalized format. */ - request?: ExtractedNodeRequestData; + normalizedRequest?: RequestEventData; /** The name of the span being sampled. */ name: string; diff --git a/packages/core/src/types-hoist/scope.ts b/packages/core/src/types-hoist/scope.ts deleted file mode 100644 index 57990d310820..000000000000 --- a/packages/core/src/types-hoist/scope.ts +++ /dev/null @@ -1,277 +0,0 @@ -import type { Attachment } from './attachment'; -import type { Breadcrumb } from './breadcrumb'; -import type { Client } from './client'; -import type { Context, Contexts } from './context'; -import type { Event, EventHint } from './event'; -import type { EventProcessor } from './eventprocessor'; -import type { Extra, Extras } from './extra'; -import type { Primitive } from './misc'; -import type { RequestSession, Session } from './session'; -import type { SeverityLevel } from './severity'; -import type { Span } from './span'; -import type { PropagationContext } from './tracing'; -import type { User } from './user'; - -/** JSDocs */ -export type CaptureContext = Scope | Partial | ((scope: Scope) => Scope); - -/** JSDocs */ -export interface ScopeContext { - user: User; - level: SeverityLevel; - extra: Extras; - contexts: Contexts; - tags: { [key: string]: Primitive }; - fingerprint: string[]; - // eslint-disable-next-line deprecation/deprecation - requestSession: RequestSession; - propagationContext: PropagationContext; -} - -export interface ScopeData { - eventProcessors: EventProcessor[]; - breadcrumbs: Breadcrumb[]; - user: User; - tags: { [key: string]: Primitive }; - extra: Extras; - contexts: Contexts; - attachments: Attachment[]; - propagationContext: PropagationContext; - sdkProcessingMetadata: { [key: string]: unknown }; - fingerprint: string[]; - level?: SeverityLevel; - transactionName?: string; - span?: Span; -} - -/** - * Holds additional event information. - */ -export interface Scope { - /** - * Update the client on the scope. - */ - setClient(client: Client | undefined): void; - - /** - * 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. - */ - getClient(): C | undefined; - - /** - * Sets the last event id on the scope. - * @param lastEventId The last event id of a captured event. - */ - setLastEventId(lastEventId: string | undefined): void; - - /** - * This is the getter for lastEventId. - * @returns The last event id of a captured event. - */ - lastEventId(): string | undefined; - - /** - * Add internal on change listener. Used for sub SDKs that need to store the scope. - * @hidden - */ - addScopeListener(callback: (scope: Scope) => void): void; - - /** Add new event processor that will be called during event processing. */ - addEventProcessor(callback: EventProcessor): this; - - /** Get the data of this scope, which is applied to an event during processing. */ - getScopeData(): ScopeData; - - /** - * Updates user context information for future events. - * - * @param user User context object to be set in the current context. Pass `null` to unset the user. - */ - setUser(user: User | null): this; - - /** - * Returns the `User` if there is one - */ - getUser(): User | undefined; - - /** - * Set an object that will be merged sent as tags data with the event. - * @param tags Tags context object to merge into current context. - */ - setTags(tags: { [key: string]: Primitive }): this; - - /** - * Set key:value that will be sent as tags data with the event. - * - * Can also be used to unset a tag by passing `undefined`. - * - * @param key String key of tag - * @param value Value of tag - */ - setTag(key: string, value: Primitive): this; - - /** - * Set an object that will be merged sent as extra data with the event. - * @param extras Extras object to merge into current context. - */ - setExtras(extras: Extras): this; - - /** - * Set key:value that will be sent as extra data with the event. - * @param key String of extra - * @param extra Any kind of data. This data will be normalized. - */ - setExtra(key: string, extra: Extra): this; - - /** - * Sets the fingerprint on the scope to send with the events. - * @param fingerprint string[] to group events in Sentry. - */ - setFingerprint(fingerprint: string[]): this; - - /** - * Sets the level on the scope for future events. - * @param level string {@link SeverityLevel} - */ - setLevel(level: SeverityLevel): this; - - /** - * Sets the transaction name on the scope so that the name of the transaction - * (e.g. taken server route or page location) is attached to future events. - * - * IMPORTANT: Calling this function does NOT change the name of the currently active - * span. If you want to change the name of the active span, use `span.updateName()` - * instead. - * - * By default, the SDK updates the scope's transaction name automatically on sensible - * occasions, such as a page navigation or when handling a new request on the server. - */ - setTransactionName(name?: string): this; - - /** - * Sets context data with the given name. - * @param name of the context - * @param context an object containing context data. This data will be normalized. Pass `null` to unset the context. - */ - setContext(name: string, context: Context | null): this; - - /** - * Returns the `Session` if there is one - */ - getSession(): Session | undefined; - - /** - * Sets the `Session` on the scope - */ - setSession(session?: Session): this; - - /** - * Returns the `RequestSession` if there is one - * - * @deprecated Use `getSession()` and `setSession()` instead of `getRequestSession()` and `setRequestSession()`; - */ - // eslint-disable-next-line deprecation/deprecation - getRequestSession(): RequestSession | undefined; - - /** - * Sets the `RequestSession` on the scope - * - * @deprecated Use `getSession()` and `setSession()` instead of `getRequestSession()` and `setRequestSession()`; - */ - // eslint-disable-next-line deprecation/deprecation - setRequestSession(requestSession?: RequestSession): this; - - /** - * Updates the scope with provided data. Can work in three variations: - * - plain object containing updatable attributes - * - Scope instance that'll extract the attributes from - * - callback function that'll receive the current scope as an argument and allow for modifications - * @param captureContext scope modifier to be used - */ - update(captureContext?: CaptureContext): this; - - /** Clears the current scope and resets its properties. */ - clear(): this; - - /** - * Adds a breadcrumb to the scope - * @param breadcrumb Breadcrumb - * @param maxBreadcrumbs number of max breadcrumbs to merged into event. - */ - addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this; - - /** - * Get the last breadcrumb. - */ - getLastBreadcrumb(): Breadcrumb | undefined; - - /** - * Clears all breadcrumbs from the scope. - */ - clearBreadcrumbs(): this; - - /** - * Adds an attachment to the scope - * @param attachment Attachment options - */ - addAttachment(attachment: Attachment): this; - - /** - * Clears attachments from the scope - */ - clearAttachments(): this; - - /** - * Add data which will be accessible during event processing but won't get sent to Sentry. - * - * TODO(v9): We should type this stricter, so that e.g. `normalizedRequest` is strictly typed. - */ - setSDKProcessingMetadata(newData: { [key: string]: unknown }): this; - - /** - * Add propagation context to the scope, used for distributed tracing - */ - setPropagationContext( - context: Omit & Partial>, - ): this; - - /** - * Get propagation context from the scope, used for distributed tracing - */ - getPropagationContext(): PropagationContext; - - /** - * Capture an exception for this scope. - * - * @param exception The exception to capture. - * @param hint Optional additional data to attach to the Sentry event. - * @returns the id of the captured Sentry event. - */ - captureException(exception: unknown, hint?: EventHint): string; - - /** - * Capture a message for this scope. - * - * @param 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. - */ - captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string; - - /** - * Capture a Sentry event for this scope. - * - * @param event The event to capture. - * @param hint Optional additional data to attach to the Sentry event. - * @returns the id of the captured event. - */ - captureEvent(event: Event, hint?: EventHint): string; - - /** - * Clone all data from this scope into a new scope. - */ - clone(): Scope; -} diff --git a/packages/core/src/types-hoist/session.ts b/packages/core/src/types-hoist/session.ts index 47cfa348acbb..1cdfef158af8 100644 --- a/packages/core/src/types-hoist/session.ts +++ b/packages/core/src/types-hoist/session.ts @@ -1,13 +1,5 @@ import type { User } from './user'; -/** - * @deprecated This type is deprecated and will be removed in the next major version of the SDK. - */ -export interface RequestSession { - // eslint-disable-next-line deprecation/deprecation - status?: RequestSessionStatus; -} - export interface Session { sid: string; did?: string | number; @@ -40,41 +32,24 @@ export type SessionContext = Partial; export type SessionStatus = 'ok' | 'exited' | 'crashed' | 'abnormal'; -/** - * @deprecated This type is deprecated and will be removed in the next major version of the SDK. - */ -export type RequestSessionStatus = 'ok' | 'errored' | 'crashed'; - /** JSDoc */ export interface SessionAggregates { attrs?: { environment?: string; release?: string; + ip_address?: string | null; }; aggregates: Array; } -/** - * @deprecated This type is deprecated and will be removed in the next major version of the SDK. - */ -export interface SessionFlusherLike { - /** - * Increments the Session Status bucket in SessionAggregates Object corresponding to the status of the session - * captured - */ - incrementSessionStatusCount(): void; - - /** Empties Aggregate Buckets and Sends them to Transport Buffer */ - flush(): void; - - /** Clears setInterval and calls flush */ - close(): void; -} - export interface AggregationCounts { + /** ISO Timestamp rounded to the second */ started: string; - errored?: number; + /** Number of sessions that did not have errors */ exited?: number; + /** Number of sessions that had handled errors */ + errored?: number; + /** Number of sessions that had unhandled errors */ crashed?: number; } diff --git a/packages/core/src/types-hoist/span.ts b/packages/core/src/types-hoist/span.ts index a2ee74fd7cfa..c74d00e54f97 100644 --- a/packages/core/src/types-hoist/span.ts +++ b/packages/core/src/types-hoist/span.ts @@ -1,5 +1,4 @@ import type { Measurements } from './measurement'; -import type { Primitive } from './misc'; import type { HrTime } from './opentelemetry'; import type { SpanStatus } from './spanStatus'; import type { TransactionSource } from './transaction'; @@ -31,20 +30,12 @@ export type SpanAttributes = Partial<{ }> & Record; -export type MetricSummary = { - min: number; - max: number; - count: number; - sum: number; - tags?: Record | undefined; -}; - /** This type is aligned with the OpenTelemetry TimeInput type. */ export type SpanTimeInput = HrTime | number | Date; /** A JSON representation of a span. */ export interface SpanJSON { - data?: { [key: string]: any }; + data: SpanAttributes; description?: string; op?: string; parent_span_id?: string; @@ -54,7 +45,6 @@ export interface SpanJSON { timestamp?: number; trace_id: string; origin?: SpanOrigin; - _metrics_summary?: Record>; profile_id?: string; exclusive_time?: number; measurements?: Measurements; @@ -234,6 +224,16 @@ export interface Span { /** * Update the name of the span. + * + * **Important:** You most likely want to use `Sentry.updateSpanName(span, name)` instead! + * + * This method will update the current span name but cannot guarantee that the new name will be + * the final name of the span. Instrumentation might still overwrite the name with an automatically + * computed name, for example in `http.server` or `db` spans. + * + * You can ensure that your name is kept and not overwritten by calling `Sentry.updateSpanName(span, name)` + * + * @param name the new name of the span */ updateName(name: string): this; diff --git a/packages/core/src/types-hoist/startSpanOptions.ts b/packages/core/src/types-hoist/startSpanOptions.ts index 35d5326e32f3..6e5fa007bde8 100644 --- a/packages/core/src/types-hoist/startSpanOptions.ts +++ b/packages/core/src/types-hoist/startSpanOptions.ts @@ -1,11 +1,21 @@ -import type { Scope } from './scope'; +import type { Scope } from '../scope'; import type { Span, SpanAttributes, SpanTimeInput } from './span'; export interface StartSpanOptions { /** A manually specified start time for the created `Span` object. */ startTime?: SpanTimeInput; - /** If defined, start this span off this scope instead off the current scope. */ + /** + * If set, start the span on a fork of this scope instead of on the current scope. + * To ensure proper span cleanup, the passed scope is cloned for the duration of the span. + * + * If you want to modify the passed scope inside the callback, calling `getCurrentScope()` + * will return the cloned scope, meaning all scope modifications will be reset once the + * callback finishes + * + * If you want to modify the passed scope and have the changes persist after the callback ends, + * modify the scope directly instead of using `getCurrentScope()` + */ scope?: Scope; /** The name of the span. */ diff --git a/packages/core/src/types-hoist/tracing.ts b/packages/core/src/types-hoist/tracing.ts index c93a70c9fdd3..e1dcfef96c6a 100644 --- a/packages/core/src/types-hoist/tracing.ts +++ b/packages/core/src/types-hoist/tracing.ts @@ -15,21 +15,20 @@ export interface PropagationContext { * Either represents the incoming `traceId` or the `traceId` generated by the current SDK, if there was no incoming trace. */ traceId: string; + /** - * Represents the execution context of the current SDK. This acts as a fallback value to associate events with a - * particular execution context when performance monitoring is disabled. - * - * The ID of a current span (if one exists) should have precedence over this value when propagating trace data. - * - * @deprecated This value will not be used anymore in the future, and should not be set or read anymore. + * A random between 0 an 1 (including 0, excluding 1) used for sampling in the current execution context. + * This should be newly generated when a new trace is started. */ - spanId: string; + sampleRand: number; + /** * Represents the sampling decision of the incoming trace. * * The current SDK should not modify this value! */ sampled?: boolean; + /** * The `parentSpanId` denotes the ID of the incoming client span. If there is no `parentSpanId` on the propagation * context, it means that the the incoming trace didn't come from a span. @@ -37,6 +36,14 @@ export interface PropagationContext { * The current SDK should not modify this value! */ parentSpanId?: string; + + /** + * A span ID that should be used for the `trace` context of various event types, and for propagation of a `parentSpanId` to downstream services, when performance is disabled or when there is no active span. + * This value should be set by the SDK in an informed way when the same span ID should be used for one unit of execution (e.g. a request, usually tied to the isolation scope). + * If this value is undefined on the propagation context, the SDK will generate a random span ID for `trace` contexts and trace propagation. + */ + propagationSpanId?: string; + /** * An undefined dsc in the propagation context means that the current SDK invocation is the head of trace and still free to modify and set the DSC for outgoing requests. * diff --git a/packages/core/src/types-hoist/transport.ts b/packages/core/src/types-hoist/transport.ts index 39741bf111de..8e0035c93137 100644 --- a/packages/core/src/types-hoist/transport.ts +++ b/packages/core/src/types-hoist/transport.ts @@ -1,4 +1,4 @@ -import type { Client } from './client'; +import type { Client } from '../client'; import type { Envelope } from './envelope'; export type TransportRequest = { diff --git a/packages/core/src/types-hoist/user.ts b/packages/core/src/types-hoist/user.ts index f559c5029825..801fb66202b1 100644 --- a/packages/core/src/types-hoist/user.ts +++ b/packages/core/src/types-hoist/user.ts @@ -4,7 +4,7 @@ export interface User { [key: string]: any; id?: string | number; - ip_address?: string; + ip_address?: string | null; email?: string; username?: string; geo?: GeoLocation; diff --git a/packages/core/src/types-hoist/wrappedfunction.ts b/packages/core/src/types-hoist/wrappedfunction.ts index 91960b0d59fb..991e05d43a4b 100644 --- a/packages/core/src/types-hoist/wrappedfunction.ts +++ b/packages/core/src/types-hoist/wrappedfunction.ts @@ -3,8 +3,6 @@ */ // eslint-disable-next-line @typescript-eslint/ban-types export type WrappedFunction = T & { - // TODO(v9): Remove this - [key: string]: any; __sentry_wrapped__?: WrappedFunction; __sentry_original__?: T; }; diff --git a/packages/core/src/utils-hoist/aggregate-errors.ts b/packages/core/src/utils-hoist/aggregate-errors.ts index 5f791183f02b..606b2d12161e 100644 --- a/packages/core/src/utils-hoist/aggregate-errors.ts +++ b/packages/core/src/utils-hoist/aggregate-errors.ts @@ -15,7 +15,7 @@ export function applyAggregateErrorsToEvent( event: Event, hint?: EventHint, ): void { - if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { + if (!event.exception?.values || !hint || !isInstanceOf(hint.originalException, Error)) { return; } diff --git a/packages/core/src/utils-hoist/array.ts b/packages/core/src/utils-hoist/array.ts deleted file mode 100644 index 412e22224156..000000000000 --- a/packages/core/src/utils-hoist/array.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type NestedArray = Array | T>; - -/** Flattens a multi-dimensional array - * - * @deprecated This function is deprecated and will be removed in the next major version. - */ -export function flatten(input: NestedArray): T[] { - const result: T[] = []; - - const flattenHelper = (input: NestedArray): void => { - input.forEach((el: T | NestedArray) => { - if (Array.isArray(el)) { - flattenHelper(el as NestedArray); - } else { - result.push(el as T); - } - }); - }; - - flattenHelper(input); - return result; -} diff --git a/packages/core/src/utils-hoist/baggage.ts b/packages/core/src/utils-hoist/baggage.ts index 5fb60af8a203..075dbf4389df 100644 --- a/packages/core/src/utils-hoist/baggage.ts +++ b/packages/core/src/utils-hoist/baggage.ts @@ -4,11 +4,6 @@ import { DEBUG_BUILD } from './debug-build'; import { isString } from './is'; import { logger } from './logger'; -/** - * @deprecated Use a `"baggage"` string directly - */ -export const BAGGAGE_HEADER_NAME = 'baggage'; - export const SENTRY_BAGGAGE_KEY_PREFIX = 'sentry-'; export const SENTRY_BAGGAGE_KEY_PREFIX_REGEX = /^sentry-/; diff --git a/packages/core/src/utils-hoist/browser.ts b/packages/core/src/utils-hoist/browser.ts index b3a5220e7c3c..bd9775c594ab 100644 --- a/packages/core/src/utils-hoist/browser.ts +++ b/packages/core/src/utils-hoist/browser.ts @@ -76,7 +76,7 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string { const out = []; - if (!elem || !elem.tagName) { + if (!elem?.tagName) { return ''; } @@ -96,12 +96,11 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string { out.push(elem.tagName.toLowerCase()); // Pairs of attribute keys defined in `serializeAttribute` and their values on element. - const keyAttrPairs = - keyAttrs && keyAttrs.length - ? keyAttrs.filter(keyAttr => elem.getAttribute(keyAttr)).map(keyAttr => [keyAttr, elem.getAttribute(keyAttr)]) - : null; + const keyAttrPairs = keyAttrs?.length + ? keyAttrs.filter(keyAttr => elem.getAttribute(keyAttr)).map(keyAttr => [keyAttr, elem.getAttribute(keyAttr)]) + : null; - if (keyAttrPairs && keyAttrPairs.length) { + if (keyAttrPairs?.length) { keyAttrPairs.forEach(keyAttrPair => { out.push(`[${keyAttrPair[0]}="${keyAttrPair[1]}"]`); }); @@ -140,30 +139,6 @@ export function getLocationHref(): string { } } -/** - * Gets a DOM element by using document.querySelector. - * - * This wrapper will first check for the existence of the function before - * actually calling it so that we don't have to take care of this check, - * every time we want to access the DOM. - * - * Reason: DOM/querySelector is not available in all environments. - * - * We have to cast to any because utils can be consumed by a variety of environments, - * and we don't want to break TS users. If you know what element will be selected by - * `document.querySelector`, specify it as part of the generic call. For example, - * `const element = getDomElement('selector');` - * - * @param selector the selector string passed on to document.querySelector - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getDomElement(selector: string): E | null { - if (WINDOW.document && WINDOW.document.querySelector) { - return WINDOW.document.querySelector(selector) as unknown as E; - } - return null; -} - /** * Given a DOM element, traverses up the tree until it finds the first ancestor node * that has the `data-sentry-component` or `data-sentry-element` attribute with `data-sentry-component` taking diff --git a/packages/core/src/utils-hoist/buildPolyfills/README.md b/packages/core/src/utils-hoist/buildPolyfills/README.md deleted file mode 100644 index 3b18ad989133..000000000000 --- a/packages/core/src/utils-hoist/buildPolyfills/README.md +++ /dev/null @@ -1,30 +0,0 @@ -## Build Polyfills - -This is a collection of syntax and import/export polyfills either copied directly from or heavily inspired by those used -by [Rollup](https://github.com/rollup/rollup) and [Sucrase](https://github.com/alangpierce/sucrase). When either tool -uses one of these polyfills during a build, it injects the function source code into each file needing the function, -which can lead to a great deal of duplication. For our builds, we have therefore implemented something similar to -[`tsc`'s `importHelpers` behavior](https://www.typescriptlang.org/tsconfig#importHelpers): Instead of leaving the -polyfills injected in multiple places, we instead replace each injected function with an `import` or `require` -statement. - -Note that not all polyfills are currently used by the SDK, but all are included here for future compatibility, should -they ever be needed. Also, since we're never going to be calling these directly from within another TS file, their types -are fairly generic. In some cases testing required more specific types, which can be found in the test files. - ---- - -_Code from both Rollup and Sucrase is used under the MIT license, copyright 2017 and 2012-2018, respectively._ - -_Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the following conditions:_ - -_The above copyright notice and this permission notice shall be included in all copies or substantial portions of the -Software._ - -_THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._ diff --git a/packages/core/src/utils-hoist/buildPolyfills/_asyncNullishCoalesce.ts b/packages/core/src/utils-hoist/buildPolyfills/_asyncNullishCoalesce.ts deleted file mode 100644 index 032b31011f96..000000000000 --- a/packages/core/src/utils-hoist/buildPolyfills/_asyncNullishCoalesce.ts +++ /dev/null @@ -1,51 +0,0 @@ -// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f -// -// The MIT License (MIT) -// -// Copyright (c) 2012-2018 various contributors (see AUTHORS) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import { _nullishCoalesce } from './_nullishCoalesce'; - -/** - * Polyfill for the nullish coalescing operator (`??`), when used in situations where at least one of the values is the - * result of an async operation. - * - * Note that the RHS is wrapped in a function so that if it's a computed value, that evaluation won't happen unless the - * LHS evaluates to a nullish value, to mimic the operator's short-circuiting behavior. - * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) - * - * @param lhs The value of the expression to the left of the `??` - * @param rhsFn A function returning the value of the expression to the right of the `??` - * @returns The LHS value, unless it's `null` or `undefined`, in which case, the RHS value - */ -export async function _asyncNullishCoalesce(lhs: unknown, rhsFn: () => unknown): Promise { - return _nullishCoalesce(lhs, rhsFn); -} - -// Sucrase version: -// async function _asyncNullishCoalesce(lhs, rhsFn) { -// if (lhs != null) { -// return lhs; -// } else { -// return await rhsFn(); -// } -// } diff --git a/packages/core/src/utils-hoist/buildPolyfills/_asyncOptionalChain.ts b/packages/core/src/utils-hoist/buildPolyfills/_asyncOptionalChain.ts deleted file mode 100644 index 37489b5c9232..000000000000 --- a/packages/core/src/utils-hoist/buildPolyfills/_asyncOptionalChain.ts +++ /dev/null @@ -1,82 +0,0 @@ -// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f -// -// The MIT License (MIT) -// -// Copyright (c) 2012-2018 various contributors (see AUTHORS) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import type { GenericFunction } from './types'; - -/** - * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, - * descriptors, and functions, for situations in which at least one part of the expression is async. - * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See - * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 - * - * @param ops Array result of expression conversion - * @returns The value of the expression - */ -export async function _asyncOptionalChain(ops: unknown[]): Promise { - let lastAccessLHS: unknown = undefined; - let value = ops[0]; - let i = 1; - while (i < ops.length) { - const op = ops[i] as string; - const fn = ops[i + 1] as (intermediateValue: unknown) => Promise; - i += 2; - // by checking for loose equality to `null`, we catch both `null` and `undefined` - if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { - // really we're meaning to return `undefined` as an actual value here, but it saves bytes not to write it - return; - } - if (op === 'access' || op === 'optionalAccess') { - lastAccessLHS = value; - value = await fn(value); - } else if (op === 'call' || op === 'optionalCall') { - value = await fn((...args: unknown[]) => (value as GenericFunction).call(lastAccessLHS, ...args)); - lastAccessLHS = undefined; - } - } - return value; -} - -// Sucrase version: -// async function _asyncOptionalChain(ops) { -// let lastAccessLHS = undefined; -// let value = ops[0]; -// let i = 1; -// while (i < ops.length) { -// const op = ops[i]; -// const fn = ops[i + 1]; -// i += 2; -// if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { -// return undefined; -// } -// if (op === 'access' || op === 'optionalAccess') { -// lastAccessLHS = value; -// value = await fn(value); -// } else if (op === 'call' || op === 'optionalCall') { -// value = await fn((...args) => value.call(lastAccessLHS, ...args)); -// lastAccessLHS = undefined; -// } -// } -// return value; -// } diff --git a/packages/core/src/utils-hoist/buildPolyfills/_asyncOptionalChainDelete.ts b/packages/core/src/utils-hoist/buildPolyfills/_asyncOptionalChainDelete.ts deleted file mode 100644 index 9cef4fd791f0..000000000000 --- a/packages/core/src/utils-hoist/buildPolyfills/_asyncOptionalChainDelete.ts +++ /dev/null @@ -1,51 +0,0 @@ -// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f -// -// The MIT License (MIT) -// -// Copyright (c) 2012-2018 various contributors (see AUTHORS) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import { _asyncOptionalChain } from './_asyncOptionalChain'; - -/** - * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, - * descriptors, and functions, in cases where the value of the expression is to be deleted. - * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See - * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 - * - * @param ops Array result of expression conversion - * @returns The return value of the `delete` operator: `true`, unless the deletion target is an own, non-configurable - * property (one which can't be deleted or turned into an accessor, and whose enumerability can't be changed), in which - * case `false`. - */ -export async function _asyncOptionalChainDelete(ops: unknown[]): Promise { - const result = (await _asyncOptionalChain(ops)) as Promise; - // If `result` is `null`, it means we didn't get to the end of the chain and so nothing was deleted (in which case, - // return `true` since that's what `delete` does when it no-ops). If it's non-null, we know the delete happened, in - // which case we return whatever the `delete` returned, which will be a boolean. - return result == null ? true : (result as Promise); -} - -// Sucrase version: -// async function asyncOptionalChainDelete(ops) { -// const result = await ASYNC_OPTIONAL_CHAIN_NAME(ops); -// return result == null ? true : result; -// } diff --git a/packages/core/src/utils-hoist/buildPolyfills/_nullishCoalesce.ts b/packages/core/src/utils-hoist/buildPolyfills/_nullishCoalesce.ts deleted file mode 100644 index a11cd469bf11..000000000000 --- a/packages/core/src/utils-hoist/buildPolyfills/_nullishCoalesce.ts +++ /dev/null @@ -1,49 +0,0 @@ -// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f -// -// The MIT License (MIT) -// -// Copyright (c) 2012-2018 various contributors (see AUTHORS) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -/** - * Polyfill for the nullish coalescing operator (`??`). - * - * Note that the RHS is wrapped in a function so that if it's a computed value, that evaluation won't happen unless the - * LHS evaluates to a nullish value, to mimic the operator's short-circuiting behavior. - * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) - * - * @param lhs The value of the expression to the left of the `??` - * @param rhsFn A function returning the value of the expression to the right of the `??` - * @returns The LHS value, unless it's `null` or `undefined`, in which case, the RHS value - */ -export function _nullishCoalesce(lhs: unknown, rhsFn: () => unknown): unknown { - // by checking for loose equality to `null`, we catch both `null` and `undefined` - return lhs != null ? lhs : rhsFn(); -} - -// Sucrase version: -// function _nullishCoalesce(lhs, rhsFn) { -// if (lhs != null) { -// return lhs; -// } else { -// return rhsFn(); -// } -// } diff --git a/packages/core/src/utils-hoist/buildPolyfills/_optionalChain.ts b/packages/core/src/utils-hoist/buildPolyfills/_optionalChain.ts deleted file mode 100644 index a7ea8338d744..000000000000 --- a/packages/core/src/utils-hoist/buildPolyfills/_optionalChain.ts +++ /dev/null @@ -1,82 +0,0 @@ -// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f -// -// The MIT License (MIT) -// -// Copyright (c) 2012-2018 various contributors (see AUTHORS) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import type { GenericFunction } from './types'; - -/** - * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, - * descriptors, and functions. - * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) - * See https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 - * - * @param ops Array result of expression conversion - * @returns The value of the expression - */ -export function _optionalChain(ops: unknown[]): unknown { - let lastAccessLHS: unknown = undefined; - let value = ops[0]; - let i = 1; - while (i < ops.length) { - const op = ops[i] as string; - const fn = ops[i + 1] as (intermediateValue: unknown) => unknown; - i += 2; - // by checking for loose equality to `null`, we catch both `null` and `undefined` - if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { - // really we're meaning to return `undefined` as an actual value here, but it saves bytes not to write it - return; - } - if (op === 'access' || op === 'optionalAccess') { - lastAccessLHS = value; - value = fn(value); - } else if (op === 'call' || op === 'optionalCall') { - value = fn((...args: unknown[]) => (value as GenericFunction).call(lastAccessLHS, ...args)); - lastAccessLHS = undefined; - } - } - return value; -} - -// Sucrase version -// function _optionalChain(ops) { -// let lastAccessLHS = undefined; -// let value = ops[0]; -// let i = 1; -// while (i < ops.length) { -// const op = ops[i]; -// const fn = ops[i + 1]; -// i += 2; -// if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { -// return undefined; -// } -// if (op === 'access' || op === 'optionalAccess') { -// lastAccessLHS = value; -// value = fn(value); -// } else if (op === 'call' || op === 'optionalCall') { -// value = fn((...args) => value.call(lastAccessLHS, ...args)); -// lastAccessLHS = undefined; -// } -// } -// return value; -// } diff --git a/packages/core/src/utils-hoist/buildPolyfills/_optionalChainDelete.ts b/packages/core/src/utils-hoist/buildPolyfills/_optionalChainDelete.ts deleted file mode 100644 index 04bffc8ab385..000000000000 --- a/packages/core/src/utils-hoist/buildPolyfills/_optionalChainDelete.ts +++ /dev/null @@ -1,52 +0,0 @@ -// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f -// -// The MIT License (MIT) -// -// Copyright (c) 2012-2018 various contributors (see AUTHORS) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import { _optionalChain } from './_optionalChain'; - -/** - * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, - * descriptors, and functions, in cases where the value of the expression is to be deleted. - * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See - * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 - * - * @param ops Array result of expression conversion - * @returns The return value of the `delete` operator: `true`, unless the deletion target is an own, non-configurable - * property (one which can't be deleted or turned into an accessor, and whose enumerability can't be changed), in which - * case `false`. - */ -export function _optionalChainDelete(ops: unknown[]): boolean { - const result = _optionalChain(ops) as boolean | null; - // If `result` is `null`, it means we didn't get to the end of the chain and so nothing was deleted (in which case, - // return `true` since that's what `delete` does when it no-ops). If it's non-null, we know the delete happened, in - // which case we return whatever the `delete` returned, which will be a boolean. - return result == null ? true : result; -} - -// Sucrase version: -// function _optionalChainDelete(ops) { -// const result = _optionalChain(ops); -// // by checking for loose equality to `null`, we catch both `null` and `undefined` -// return result == null ? true : result; -// } diff --git a/packages/core/src/utils-hoist/buildPolyfills/index.ts b/packages/core/src/utils-hoist/buildPolyfills/index.ts deleted file mode 100644 index 2017dcbd9592..000000000000 --- a/packages/core/src/utils-hoist/buildPolyfills/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { _asyncNullishCoalesce } from './_asyncNullishCoalesce'; -export { _asyncOptionalChain } from './_asyncOptionalChain'; -export { _asyncOptionalChainDelete } from './_asyncOptionalChainDelete'; -export { _nullishCoalesce } from './_nullishCoalesce'; -export { _optionalChain } from './_optionalChain'; -export { _optionalChainDelete } from './_optionalChainDelete'; diff --git a/packages/core/src/utils-hoist/buildPolyfills/types.ts b/packages/core/src/utils-hoist/buildPolyfills/types.ts deleted file mode 100644 index 10e2f4a944a4..000000000000 --- a/packages/core/src/utils-hoist/buildPolyfills/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Primitive } from '../../types-hoist'; - -export type GenericObject = { [key: string]: Value }; -export type GenericFunction = (...args: unknown[]) => Value; -export type Value = Primitive | GenericFunction | GenericObject; - -export type RequireResult = GenericObject | (GenericFunction & GenericObject); diff --git a/packages/core/src/utils-hoist/cache.ts b/packages/core/src/utils-hoist/cache.ts deleted file mode 100644 index 376f8ef970cc..000000000000 --- a/packages/core/src/utils-hoist/cache.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Creates a cache that evicts keys in fifo order - * @param size {Number} - * - * @deprecated This function is deprecated and will be removed in the next major version. - */ -export function makeFifoCache( - size: number, -): { - get: (key: Key) => Value | undefined; - add: (key: Key, value: Value) => void; - delete: (key: Key) => boolean; - clear: () => void; - size: () => number; -} { - // Maintain a fifo queue of keys, we cannot rely on Object.keys as the browser may not support it. - let evictionOrder: Key[] = []; - let cache: Record = {}; - - return { - add(key: Key, value: Value) { - while (evictionOrder.length >= size) { - // shift is O(n) but this is small size and only happens if we are - // exceeding the cache size so it should be fine. - const evictCandidate = evictionOrder.shift(); - - if (evictCandidate !== undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete cache[evictCandidate]; - } - } - - // in case we have a collision, delete the old key. - if (cache[key]) { - this.delete(key); - } - - evictionOrder.push(key); - cache[key] = value; - }, - clear() { - cache = {}; - evictionOrder = []; - }, - get(key: Key): Value | undefined { - return cache[key]; - }, - size() { - return evictionOrder.length; - }, - // Delete cache key and return true if it existed, false otherwise. - delete(key: Key): boolean { - if (!cache[key]) { - return false; - } - - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete cache[key]; - - for (let i = 0; i < evictionOrder.length; i++) { - if (evictionOrder[i] === key) { - evictionOrder.splice(i, 1); - break; - } - } - - return true; - }, - }; -} diff --git a/packages/core/src/utils-hoist/debug-ids.ts b/packages/core/src/utils-hoist/debug-ids.ts index 859f8c10ba2b..055d3aec9fba 100644 --- a/packages/core/src/utils-hoist/debug-ids.ts +++ b/packages/core/src/utils-hoist/debug-ids.ts @@ -42,7 +42,7 @@ export function getFilenameToDebugIdMap(stackParser: StackParser): Record= 0; i--) { const stackFrame = parsedStack[i]; - const filename = stackFrame && stackFrame.filename; + const filename = stackFrame?.filename; const debugId = debugIdMap[stackKey]; if (filename && debugId) { diff --git a/packages/core/src/utils-hoist/envelope.ts b/packages/core/src/utils-hoist/envelope.ts index be640b90ad4f..46512850cefc 100644 --- a/packages/core/src/utils-hoist/envelope.ts +++ b/packages/core/src/utils-hoist/envelope.ts @@ -1,3 +1,4 @@ +import { getSentryCarrier } from '../carrier'; import type { Attachment, AttachmentItem, @@ -74,18 +75,16 @@ export function envelopeContainsItemType(envelope: Envelope, types: EnvelopeItem * Encode a string to UTF8 array. */ function encodeUTF8(input: string): Uint8Array { - return GLOBAL_OBJ.__SENTRY__ && GLOBAL_OBJ.__SENTRY__.encodePolyfill - ? GLOBAL_OBJ.__SENTRY__.encodePolyfill(input) - : new TextEncoder().encode(input); + const carrier = getSentryCarrier(GLOBAL_OBJ); + return carrier.encodePolyfill ? carrier.encodePolyfill(input) : new TextEncoder().encode(input); } /** * Decode a UTF8 array to string. */ function decodeUTF8(input: Uint8Array): string { - return GLOBAL_OBJ.__SENTRY__ && GLOBAL_OBJ.__SENTRY__.decodePolyfill - ? GLOBAL_OBJ.__SENTRY__.decodePolyfill(input) - : new TextDecoder().decode(input); + const carrier = getSentryCarrier(GLOBAL_OBJ); + return carrier.decodePolyfill ? carrier.decodePolyfill(input) : new TextDecoder().decode(input); } /** @@ -223,7 +222,6 @@ const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record = { check_in: 'monitor', feedback: 'feedback', span: 'span', - statsd: 'metric_bucket', raw_security: 'security', }; @@ -236,7 +234,7 @@ export function envelopeItemTypeToDataCategory(type: EnvelopeItemType): DataCate /** Extracts the minimal SDK info from the metadata or an events */ export function getSdkMetadataForEnvelopeHeader(metadataOrEvent?: SdkMetadata | Event): SdkInfo | undefined { - if (!metadataOrEvent || !metadataOrEvent.sdk) { + if (!metadataOrEvent?.sdk) { return; } const { name, version } = metadataOrEvent.sdk; @@ -253,7 +251,7 @@ export function createEventEnvelopeHeaders( tunnel: string | undefined, dsn?: DsnComponents, ): EventEnvelopeHeaders { - const dynamicSamplingContext = event.sdkProcessingMetadata && event.sdkProcessingMetadata.dynamicSamplingContext; + const dynamicSamplingContext = event.sdkProcessingMetadata?.dynamicSamplingContext; return { event_id: event.event_id as string, sent_at: new Date().toISOString(), diff --git a/packages/core/src/utils-hoist/eventbuilder.ts b/packages/core/src/utils-hoist/eventbuilder.ts index 84d6e722ad7b..8aad56c229a3 100644 --- a/packages/core/src/utils-hoist/eventbuilder.ts +++ b/packages/core/src/utils-hoist/eventbuilder.ts @@ -1,5 +1,5 @@ +import type { Client } from '../client'; import type { - Client, Event, EventHint, Exception, @@ -10,7 +10,6 @@ import type { StackFrame, StackParser, } from '../types-hoist'; - import { isError, isErrorEvent, isParameterizedString, isPlainObject } from './is'; import { addExceptionMechanism, addExceptionTypeValue } from './misc'; import { normalizeToSize } from './normalize'; @@ -105,7 +104,7 @@ function getException( mechanism.synthetic = true; if (isPlainObject(exception)) { - const normalizeDepth = client && client.getOptions().normalizeDepth; + const normalizeDepth = client?.getOptions().normalizeDepth; const extras = { ['__serialized__']: normalizeToSize(exception as Record, normalizeDepth) }; const errorFromProp = getErrorPropertyFromObject(exception); @@ -114,7 +113,7 @@ function getException( } const message = getMessageForObject(exception); - const ex = (hint && hint.syntheticException) || new Error(message); + const ex = hint?.syntheticException || new Error(message); ex.message = message; return [ex, extras]; @@ -122,7 +121,7 @@ function getException( // This handles when someone does: `throw "something awesome";` // We use synthesized Error here so we can extract a (rough) stack trace. - const ex = (hint && hint.syntheticException) || new Error(exception as string); + const ex = hint?.syntheticException || new Error(exception as string); ex.message = `${exception}`; return [ex, undefined]; @@ -138,8 +137,7 @@ export function eventFromUnknownInput( exception: unknown, hint?: EventHint, ): Event { - const providedMechanism: Mechanism | undefined = - hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; + const providedMechanism: Mechanism | undefined = hint?.data && (hint.data as { mechanism: Mechanism }).mechanism; const mechanism: Mechanism = providedMechanism || { handled: true, type: 'generic', @@ -162,7 +160,7 @@ export function eventFromUnknownInput( return { ...event, - event_id: hint && hint.event_id, + event_id: hint?.event_id, }; } @@ -178,11 +176,11 @@ export function eventFromMessage( attachStacktrace?: boolean, ): Event { const event: Event = { - event_id: hint && hint.event_id, + event_id: hint?.event_id, level, }; - if (attachStacktrace && hint && hint.syntheticException) { + if (attachStacktrace && hint?.syntheticException) { const frames = parseStackFrames(stackParser, hint.syntheticException); if (frames.length) { event.exception = { diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index e53cd0edb59b..a593b72e73ad 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -1,11 +1,13 @@ export { applyAggregateErrorsToEvent } from './aggregate-errors'; -// eslint-disable-next-line deprecation/deprecation -export { flatten } from './array'; export { getBreadcrumbLogLevelFromHttpStatusCode } from './breadcrumb-log-level'; -export { getComponentName, getDomElement, getLocationHref, htmlTreeAsString } from './browser'; +export { + getComponentName, + getLocationHref, + htmlTreeAsString, +} from './browser'; export { dsnFromString, dsnToString, makeDsn } from './dsn'; export { SentryError } from './error'; -export { GLOBAL_OBJ, getGlobalSingleton } from './worldwide'; +export { GLOBAL_OBJ } from './worldwide'; export type { InternalGlobal } from './worldwide'; export { addConsoleInstrumentationHandler } from './instrument/console'; export { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './instrument/fetch'; @@ -36,21 +38,16 @@ export { } from './is'; export { isBrowser } from './isBrowser'; export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods } from './logger'; -// eslint-disable-next-line deprecation/deprecation -export { memoBuilder } from './memo'; export { addContextToFrame, addExceptionMechanism, addExceptionTypeValue, - // eslint-disable-next-line deprecation/deprecation - arrayify, checkOrSetAlreadyCaught, getEventDescription, parseSemver, uuid4, } from './misc'; -// eslint-disable-next-line deprecation/deprecation -export { dynamicRequire, isNodeEnv, loadModule } from './node'; +export { isNodeEnv, loadModule } from './node'; export { normalize, normalizeToSize, normalizeUrlToBase } from './normalize'; export { addNonEnumerableProperty, @@ -61,37 +58,12 @@ export { getOriginalFunction, markFunctionWrapped, objectify, - // eslint-disable-next-line deprecation/deprecation - urlEncode, } from './object'; export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './path'; export { makePromiseBuffer } from './promisebuffer'; export type { PromiseBuffer } from './promisebuffer'; -// TODO: Remove requestdata export once equivalent integration is used everywhere -export { - DEFAULT_USER_INCLUDES, - addNormalizedRequestDataToEvent, - // eslint-disable-next-line deprecation/deprecation - addRequestDataToEvent, - // eslint-disable-next-line deprecation/deprecation - extractPathForTransaction, - // eslint-disable-next-line deprecation/deprecation - extractRequestData, - winterCGHeadersToDict, - winterCGRequestToRequestData, - httpRequestToRequestData, - extractQueryParamsFromUrl, - headersToDict, -} from './requestdata'; -export type { - AddRequestDataToEventOptions, - // eslint-disable-next-line deprecation/deprecation - TransactionNamingScheme, -} from './requestdata'; - -// eslint-disable-next-line deprecation/deprecation -export { severityLevelFromString, validSeverityLevels } from './severity'; +export { severityLevelFromString } from './severity'; export { UNKNOWN_FUNCTION, createStackParser, @@ -108,14 +80,13 @@ export { supportsDOMException, supportsErrorEvent, supportsFetch, + supportsHistory, supportsNativeFetch, supportsReferrerPolicy, supportsReportingObserver, } from './supports'; export { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './syncpromise'; export { - // eslint-disable-next-line deprecation/deprecation - _browserPerformanceTimeOriginMode, browserPerformanceTimeOrigin, dateTimestampInSeconds, timestampInSeconds, @@ -151,8 +122,6 @@ export { } from './ratelimit'; export type { RateLimits } from './ratelimit'; export { - // eslint-disable-next-line deprecation/deprecation - BAGGAGE_HEADER_NAME, MAX_BAGGAGE_STRING_LENGTH, SENTRY_BAGGAGE_KEY_PREFIX, SENTRY_BAGGAGE_KEY_PREFIX_REGEX, @@ -161,16 +130,11 @@ export { parseBaggageHeader, } from './baggage'; -// eslint-disable-next-line deprecation/deprecation -export { getNumberOfUrlSegments, getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from './url'; -// eslint-disable-next-line deprecation/deprecation -export { makeFifoCache } from './cache'; +export { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from './url'; export { eventFromMessage, eventFromUnknownInput, exceptionFromError, parseStackFrames } from './eventbuilder'; export { callFrameToStackFrame, watchdogTimer } from './anr'; export { LRUMap } from './lru'; export { - // eslint-disable-next-line deprecation/deprecation - generatePropagationContext, generateTraceId, generateSpanId, } from './propagationContext'; @@ -178,11 +142,3 @@ export { vercelWaitUntil } from './vercelWaitUntil'; export { SDK_VERSION } from './version'; export { getDebugImagesForResources, getFilenameToDebugIdMap } from './debug-ids'; export { escapeStringForRegex } from './vendor/escapeStringForRegex'; -export { supportsHistory } from './vendor/supportsHistory'; - -export { _asyncNullishCoalesce } from './buildPolyfills/_asyncNullishCoalesce'; -export { _asyncOptionalChain } from './buildPolyfills/_asyncOptionalChain'; -export { _asyncOptionalChainDelete } from './buildPolyfills/_asyncOptionalChainDelete'; -export { _nullishCoalesce } from './buildPolyfills/_nullishCoalesce'; -export { _optionalChain } from './buildPolyfills/_optionalChain'; -export { _optionalChainDelete } from './buildPolyfills/_optionalChainDelete'; diff --git a/packages/core/src/utils-hoist/instrument/console.ts b/packages/core/src/utils-hoist/instrument/console.ts index 955407e5573b..2fcb8e91b14a 100644 --- a/packages/core/src/utils-hoist/instrument/console.ts +++ b/packages/core/src/utils-hoist/instrument/console.ts @@ -37,7 +37,7 @@ function instrumentConsole(): void { triggerHandlers('console', handlerData); const log = originalConsoleMethods[level]; - log && log.apply(GLOBAL_OBJ.console, args); + log?.apply(GLOBAL_OBJ.console, args); }; }); }); diff --git a/packages/core/src/utils-hoist/instrument/fetch.ts b/packages/core/src/utils-hoist/instrument/fetch.ts index 954ab50a7536..f3eee711d26d 100644 --- a/packages/core/src/utils-hoist/instrument/fetch.ts +++ b/packages/core/src/utils-hoist/instrument/fetch.ts @@ -118,7 +118,7 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat } async function resolveResponse(res: Response | undefined, onFinishedResolving: () => void): Promise { - if (res && res.body) { + if (res?.body) { const body = res.body; const responseReader = body.getReader(); diff --git a/packages/core/src/utils-hoist/is.ts b/packages/core/src/utils-hoist/is.ts index 28ebfd7be2f7..cfa9bc141e20 100644 --- a/packages/core/src/utils-hoist/is.ts +++ b/packages/core/src/utils-hoist/is.ts @@ -155,7 +155,7 @@ export function isRegExp(wat: unknown): wat is RegExp { */ export function isThenable(wat: any): wat is PromiseLike { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return Boolean(wat && wat.then && typeof wat.then === 'function'); + return Boolean(wat?.then && typeof wat.then === 'function'); } /** diff --git a/packages/core/src/utils-hoist/isBrowser.ts b/packages/core/src/utils-hoist/isBrowser.ts index b77d65c0f3ff..f2052025ae7b 100644 --- a/packages/core/src/utils-hoist/isBrowser.ts +++ b/packages/core/src/utils-hoist/isBrowser.ts @@ -14,5 +14,5 @@ type ElectronProcess = { type?: string }; // Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them function isElectronNodeRenderer(): boolean { const process = (GLOBAL_OBJ as typeof GLOBAL_OBJ & { process?: ElectronProcess }).process; - return !!process && process.type === 'renderer'; + return process?.type === 'renderer'; } diff --git a/packages/core/src/utils-hoist/logger.ts b/packages/core/src/utils-hoist/logger.ts index 90d306f11434..ee642d44582d 100644 --- a/packages/core/src/utils-hoist/logger.ts +++ b/packages/core/src/utils-hoist/logger.ts @@ -1,7 +1,7 @@ +import { getGlobalSingleton } from '../carrier'; import type { ConsoleLevel } from '../types-hoist'; - import { DEBUG_BUILD } from './debug-build'; -import { GLOBAL_OBJ, getGlobalSingleton } from './worldwide'; +import { GLOBAL_OBJ } from './worldwide'; /** Prefix for logging strings */ const PREFIX = 'Sentry Logger '; @@ -24,7 +24,7 @@ export const originalConsoleMethods: { [key in ConsoleLevel]?: (...args: unknown[]) => void; } = {}; -/** JSDoc */ +/** A Sentry Logger instance. */ export interface Logger extends LoggerConsoleMethods { disable(): void; enable(): void; diff --git a/packages/core/src/utils-hoist/memo.ts b/packages/core/src/utils-hoist/memo.ts deleted file mode 100644 index f7303bd44ece..000000000000 --- a/packages/core/src/utils-hoist/memo.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export type MemoFunc = [ - // memoize - (obj: any) => boolean, - // unmemoize - (obj: any) => void, -]; - -/** - * Helper to decycle json objects - * - * @deprecated This function is deprecated and will be removed in the next major version. - */ -// TODO(v9): Move this function into normalize() directly -export function memoBuilder(): MemoFunc { - const hasWeakSet = typeof WeakSet === 'function'; - const inner: any = hasWeakSet ? new WeakSet() : []; - function memoize(obj: any): boolean { - if (hasWeakSet) { - if (inner.has(obj)) { - return true; - } - inner.add(obj); - return false; - } - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < inner.length; i++) { - const value = inner[i]; - if (value === obj) { - return true; - } - } - inner.push(obj); - return false; - } - - function unmemoize(obj: any): void { - if (hasWeakSet) { - inner.delete(obj); - } else { - for (let i = 0; i < inner.length; i++) { - if (inner[i] === obj) { - inner.splice(i, 1); - break; - } - } - } - } - return [memoize, unmemoize]; -} diff --git a/packages/core/src/utils-hoist/misc.ts b/packages/core/src/utils-hoist/misc.ts index 58a416b0ae31..853e11a09894 100644 --- a/packages/core/src/utils-hoist/misc.ts +++ b/packages/core/src/utils-hoist/misc.ts @@ -26,10 +26,10 @@ export function uuid4(): string { let getRandomByte = (): number => Math.random() * 16; try { - if (crypto && crypto.randomUUID) { + if (crypto?.randomUUID) { return crypto.randomUUID().replace(/-/g, ''); } - if (crypto && crypto.getRandomValues) { + if (crypto?.getRandomValues) { getRandomByte = () => { // crypto.getRandomValues might return undefined instead of the typed array // in old Chromium versions (e.g. 23.0.1235.0 (151422)) @@ -55,7 +55,7 @@ export function uuid4(): string { } function getFirstException(event: Event): Exception | undefined { - return event.exception && event.exception.values ? event.exception.values[0] : undefined; + return event.exception?.values?.[0]; } /** @@ -115,7 +115,7 @@ export function addExceptionMechanism(event: Event, newMechanism?: Partial(maybeArray: T | T[]): T[] { - return Array.isArray(maybeArray) ? maybeArray : [maybeArray]; -} diff --git a/packages/core/src/utils-hoist/node-stack-trace.ts b/packages/core/src/utils-hoist/node-stack-trace.ts index 46f036cb0270..56e94a0457f5 100644 --- a/packages/core/src/utils-hoist/node-stack-trace.ts +++ b/packages/core/src/utils-hoist/node-stack-trace.ts @@ -100,11 +100,11 @@ export function node(getModule?: GetModuleFn): StackLineParserFn { functionName = typeName ? `${typeName}.${methodName}` : methodName; } - let filename = lineMatch[2] && lineMatch[2].startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2]; + let filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2]; const isNative = lineMatch[5] === 'native'; // If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo` - if (filename && filename.match(/\/[A-Z]:/)) { + if (filename?.match(/\/[A-Z]:/)) { filename = filename.slice(1); } diff --git a/packages/core/src/utils-hoist/node.ts b/packages/core/src/utils-hoist/node.ts index 3805248bdedd..a0311efc7a93 100644 --- a/packages/core/src/utils-hoist/node.ts +++ b/packages/core/src/utils-hoist/node.ts @@ -23,10 +23,9 @@ export function isNodeEnv(): boolean { * Requires a module which is protected against bundler minification. * * @param request The module path to resolve - * @deprecated This function will be removed in the next major version. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function dynamicRequire(mod: any, request: string): any { +function dynamicRequire(mod: any, request: string): any { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return mod.require(request); } @@ -48,7 +47,6 @@ export function loadModule(moduleName: string): T | undefined { let mod: T | undefined; try { - // eslint-disable-next-line deprecation/deprecation mod = dynamicRequire(module, moduleName); } catch (e) { // no-empty @@ -56,9 +54,7 @@ export function loadModule(moduleName: string): T | undefined { if (!mod) { try { - // eslint-disable-next-line deprecation/deprecation const { cwd } = dynamicRequire(module, 'process'); - // eslint-disable-next-line deprecation/deprecation mod = dynamicRequire(module, `${cwd()}/node_modules/${moduleName}`) as T; } catch (e) { // no-empty diff --git a/packages/core/src/utils-hoist/normalize.ts b/packages/core/src/utils-hoist/normalize.ts index c1e8e2c630ad..254aae87c97b 100644 --- a/packages/core/src/utils-hoist/normalize.ts +++ b/packages/core/src/utils-hoist/normalize.ts @@ -1,8 +1,6 @@ import type { Primitive } from '../types-hoist'; import { isSyntheticEvent, isVueViewModel } from './is'; -import type { MemoFunc } from './memo'; -import { memoBuilder } from './memo'; import { convertToPlainObject } from './object'; import { getFunctionName } from './stacktrace'; @@ -13,6 +11,13 @@ type Prototype = { constructor: (...args: unknown[]) => unknown }; // might be arrays. type ObjOrArray = { [key: string]: T }; +type MemoFunc = [ + // memoize + (obj: object) => boolean, + // unmemoize + (obj: object) => void, +]; + /** * Recursively normalizes the given object. * @@ -74,8 +79,7 @@ function visit( value: unknown, depth: number = +Infinity, maxProperties: number = +Infinity, - // eslint-disable-next-line deprecation/deprecation - memo: MemoFunc = memoBuilder(), + memo = memoBuilder(), ): Primitive | ObjOrArray { const [memoize, unmemoize] = memo; @@ -304,3 +308,22 @@ export function normalizeUrlToBase(url: string, basePath: string): string { .replace(new RegExp(`(file://)?/*${escapedBase}/*`, 'ig'), 'app:///') ); } + +/** + * Helper to decycle json objects + */ +function memoBuilder(): MemoFunc { + const inner = new WeakSet(); + function memoize(obj: object): boolean { + if (inner.has(obj)) { + return true; + } + inner.add(obj); + return false; + } + + function unmemoize(obj: object): void { + inner.delete(obj); + } + return [memoize, unmemoize]; +} diff --git a/packages/core/src/utils-hoist/object.ts b/packages/core/src/utils-hoist/object.ts index 7d779cf6e211..31d1d862d01f 100644 --- a/packages/core/src/utils-hoist/object.ts +++ b/packages/core/src/utils-hoist/object.ts @@ -86,21 +86,6 @@ export function getOriginalFunction(func: WrappedFunction return func.__sentry_original__; } -/** - * Encodes given object into url-friendly format - * - * @param object An object that contains serializable values - * @returns string Encoded - * - * @deprecated This function is deprecated and will be removed in the next major version of the SDK. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function urlEncode(object: { [key: string]: any }): string { - return Object.entries(object) - .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) - .join('&'); -} - /** * Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their * non-enumerable properties attached. diff --git a/packages/core/src/utils-hoist/propagationContext.ts b/packages/core/src/utils-hoist/propagationContext.ts index 9c045034606e..c95fa7c00c12 100644 --- a/packages/core/src/utils-hoist/propagationContext.ts +++ b/packages/core/src/utils-hoist/propagationContext.ts @@ -1,18 +1,5 @@ -import type { PropagationContext } from '../types-hoist'; import { uuid4 } from './misc'; -/** - * Returns a new minimal propagation context. - * - * @deprecated Use `generateTraceId` and `generateSpanId` instead. - */ -export function generatePropagationContext(): PropagationContext { - return { - traceId: generateTraceId(), - spanId: generateSpanId(), - }; -} - /** * Generate a random, valid trace ID. */ diff --git a/packages/core/src/utils-hoist/ratelimit.ts b/packages/core/src/utils-hoist/ratelimit.ts index db2053bdabd6..501621245dd0 100644 --- a/packages/core/src/utils-hoist/ratelimit.ts +++ b/packages/core/src/utils-hoist/ratelimit.ts @@ -59,8 +59,8 @@ export function updateRateLimits( // "The name is case-insensitive." // https://developer.mozilla.org/en-US/docs/Web/API/Headers/get - const rateLimitHeader = headers && headers['x-sentry-rate-limits']; - const retryAfterHeader = headers && headers['retry-after']; + const rateLimitHeader = headers?.['x-sentry-rate-limits']; + const retryAfterHeader = headers?.['retry-after']; if (rateLimitHeader) { /** diff --git a/packages/core/src/utils-hoist/requestdata.ts b/packages/core/src/utils-hoist/requestdata.ts deleted file mode 100644 index bff0f3f629bd..000000000000 --- a/packages/core/src/utils-hoist/requestdata.ts +++ /dev/null @@ -1,562 +0,0 @@ -/* eslint-disable max-lines */ -import type { - Event, - ExtractedNodeRequestData, - PolymorphicRequest, - RequestEventData, - TransactionSource, - WebFetchHeaders, - WebFetchRequest, -} from '../types-hoist'; - -import { parseCookie } from './cookie'; -import { DEBUG_BUILD } from './debug-build'; -import { isPlainObject, isString } from './is'; -import { logger } from './logger'; -import { normalize } from './normalize'; -import { dropUndefinedKeys } from './object'; -import { truncate } from './string'; -import { stripUrlQueryAndFragment } from './url'; -import { getClientIPAddress, ipHeaderNames } from './vendor/getIpAddress'; - -const DEFAULT_INCLUDES = { - ip: false, - request: true, - user: true, -}; -const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; -export const DEFAULT_USER_INCLUDES = ['id', 'username', 'email']; - -/** - * Options deciding what parts of the request to use when enhancing an event - */ -export type AddRequestDataToEventOptions = { - /** Flags controlling whether each type of data should be added to the event */ - include?: { - ip?: boolean; - request?: boolean | Array<(typeof DEFAULT_REQUEST_INCLUDES)[number]>; - /** @deprecated This option will be removed in v9. It does not do anything anymore, the `transcation` is set in other places. */ - // eslint-disable-next-line deprecation/deprecation - transaction?: boolean | TransactionNamingScheme; - user?: boolean | Array<(typeof DEFAULT_USER_INCLUDES)[number]>; - }; - - /** Injected platform-specific dependencies */ - deps?: { - cookie: { - parse: (cookieStr: string) => Record; - }; - url: { - parse: (urlStr: string) => { - query: string | null; - }; - }; - }; -}; - -/** - * @deprecated This type will be removed in v9. It is not in use anymore. - */ -export type TransactionNamingScheme = 'path' | 'methodPath' | 'handler'; - -/** - * Extracts a complete and parameterized path from the request object and uses it to construct transaction name. - * If the parameterized transaction name cannot be extracted, we fall back to the raw URL. - * - * Additionally, this function determines and returns the transaction name source - * - * eg. GET /mountpoint/user/:id - * - * @param req A request object - * @param options What to include in the transaction name (method, path, or a custom route name to be - * used instead of the request's route) - * - * @returns A tuple of the fully constructed transaction name [0] and its source [1] (can be either 'route' or 'url') - * @deprecated This method will be removed in v9. It is not in use anymore. - */ -export function extractPathForTransaction( - req: PolymorphicRequest, - options: { path?: boolean; method?: boolean; customRoute?: string } = {}, -): [string, TransactionSource] { - const method = req.method && req.method.toUpperCase(); - - let path = ''; - let source: TransactionSource = 'url'; - - // Check to see if there's a parameterized route we can use (as there is in Express) - if (options.customRoute || req.route) { - path = options.customRoute || `${req.baseUrl || ''}${req.route && req.route.path}`; - source = 'route'; - } - - // Otherwise, just take the original URL - else if (req.originalUrl || req.url) { - path = stripUrlQueryAndFragment(req.originalUrl || req.url || ''); - } - - let name = ''; - if (options.method && method) { - name += method; - } - if (options.method && options.path) { - name += ' '; - } - if (options.path && path) { - name += path; - } - - return [name, source]; -} - -function extractUserData( - user: { - [key: string]: unknown; - }, - keys: boolean | string[], -): { [key: string]: unknown } { - const extractedUser: { [key: string]: unknown } = {}; - const attributes = Array.isArray(keys) ? keys : DEFAULT_USER_INCLUDES; - - attributes.forEach(key => { - if (user && key in user) { - extractedUser[key] = user[key]; - } - }); - - return extractedUser; -} - -/** - * Normalize data from the request object, accounting for framework differences. - * - * @param req The request object from which to extract data - * @param options.include An optional array of keys to include in the normalized data. Defaults to - * DEFAULT_REQUEST_INCLUDES if not provided. - * @param options.deps Injected, platform-specific dependencies - * @returns An object containing normalized request data - * - * @deprecated Instead manually normalize the request data into a format that fits `addNormalizedRequestDataToEvent`. - */ -export function extractRequestData( - req: PolymorphicRequest, - options: { - include?: string[]; - } = {}, -): ExtractedNodeRequestData { - const { include = DEFAULT_REQUEST_INCLUDES } = options; - const requestData: { [key: string]: unknown } = {}; - - // headers: - // node, express, koa, nextjs: req.headers - const headers = (req.headers || {}) as typeof req.headers & { - host?: string; - cookie?: string; - }; - // method: - // node, express, koa, nextjs: req.method - const method = req.method; - // host: - // express: req.hostname in > 4 and req.host in < 4 - // koa: req.host - // node, nextjs: req.headers.host - // Express 4 mistakenly strips off port number from req.host / req.hostname so we can't rely on them - // See: https://github.com/expressjs/express/issues/3047#issuecomment-236653223 - // Also: https://github.com/getsentry/sentry-javascript/issues/1917 - const host = headers.host || req.hostname || req.host || ''; - // protocol: - // node, nextjs: - // express, koa: req.protocol - const protocol = req.protocol === 'https' || (req.socket && req.socket.encrypted) ? 'https' : 'http'; - // url (including path and query string): - // node, express: req.originalUrl - // koa, nextjs: req.url - const originalUrl = req.originalUrl || req.url || ''; - // absolute url - const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`; - include.forEach(key => { - switch (key) { - case 'headers': { - requestData.headers = headers; - - // Remove the Cookie header in case cookie data should not be included in the event - if (!include.includes('cookies')) { - delete (requestData.headers as { cookie?: string }).cookie; - } - - // Remove IP headers in case IP data should not be included in the event - if (!include.includes('ip')) { - ipHeaderNames.forEach(ipHeaderName => { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete (requestData.headers as Record)[ipHeaderName]; - }); - } - - break; - } - case 'method': { - requestData.method = method; - break; - } - case 'url': { - requestData.url = absoluteUrl; - break; - } - case 'cookies': { - // cookies: - // node, express, koa: req.headers.cookie - // vercel, sails.js, express (w/ cookie middleware), nextjs: req.cookies - requestData.cookies = - // TODO (v8 / #5257): We're only sending the empty object for backwards compatibility, so the last bit can - // come off in v8 - req.cookies || (headers.cookie && parseCookie(headers.cookie)) || {}; - break; - } - case 'query_string': { - // query string: - // node: req.url (raw) - // express, koa, nextjs: req.query - requestData.query_string = extractQueryParams(req); - break; - } - case 'data': { - if (method === 'GET' || method === 'HEAD') { - break; - } - // NOTE: As of v8, request is (unless a user sets this manually) ALWAYS a http request - // Which does not have a body by default - // However, in our http instrumentation, we patch the request to capture the body and store it on the - // request as `.body` anyhow - // In v9, we may update requestData to only work with plain http requests - // body data: - // express, koa, nextjs: req.body - // - // when using node by itself, you have to read the incoming stream(see - // https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know - // where they're going to store the final result, so they'll have to capture this data themselves - const body = req.body; - if (body !== undefined) { - const stringBody: string = isString(body) - ? body - : isPlainObject(body) - ? JSON.stringify(normalize(body)) - : truncate(`${body}`, 1024); - if (stringBody) { - requestData.data = stringBody; - } - } - break; - } - default: { - if ({}.hasOwnProperty.call(req, key)) { - requestData[key] = (req as { [key: string]: unknown })[key]; - } - } - } - }); - - return requestData; -} - -/** - * Add already normalized request data to an event. - * This mutates the passed in event. - */ -export function addNormalizedRequestDataToEvent( - event: Event, - req: RequestEventData, - // This is non-standard data that is not part of the regular HTTP request - additionalData: { ipAddress?: string; user?: Record }, - options: AddRequestDataToEventOptions, -): void { - const include = { - ...DEFAULT_INCLUDES, - ...(options && options.include), - }; - - if (include.request) { - const includeRequest = Array.isArray(include.request) ? [...include.request] : [...DEFAULT_REQUEST_INCLUDES]; - if (include.ip) { - includeRequest.push('ip'); - } - - const extractedRequestData = extractNormalizedRequestData(req, { include: includeRequest }); - - event.request = { - ...event.request, - ...extractedRequestData, - }; - } - - if (include.user) { - const extractedUser = - additionalData.user && isPlainObject(additionalData.user) - ? extractUserData(additionalData.user, include.user) - : {}; - - if (Object.keys(extractedUser).length) { - event.user = { - ...event.user, - ...extractedUser, - }; - } - } - - if (include.ip) { - const ip = (req.headers && getClientIPAddress(req.headers)) || additionalData.ipAddress; - if (ip) { - event.user = { - ...event.user, - ip_address: ip, - }; - } - } -} - -/** - * Add data from the given request to the given event - * - * @param event The event to which the request data will be added - * @param req Request object - * @param options.include Flags to control what data is included - * @param options.deps Injected platform-specific dependencies - * @returns The mutated `Event` object - * - * @deprecated Use `addNormalizedRequestDataToEvent` instead. - */ -export function addRequestDataToEvent( - event: Event, - req: PolymorphicRequest, - options?: AddRequestDataToEventOptions, -): Event { - const include = { - ...DEFAULT_INCLUDES, - ...(options && options.include), - }; - - if (include.request) { - const includeRequest = Array.isArray(include.request) ? [...include.request] : [...DEFAULT_REQUEST_INCLUDES]; - if (include.ip) { - includeRequest.push('ip'); - } - - // eslint-disable-next-line deprecation/deprecation - const extractedRequestData = extractRequestData(req, { include: includeRequest }); - - event.request = { - ...event.request, - ...extractedRequestData, - }; - } - - if (include.user) { - const extractedUser = req.user && isPlainObject(req.user) ? extractUserData(req.user, include.user) : {}; - - if (Object.keys(extractedUser).length) { - event.user = { - ...event.user, - ...extractedUser, - }; - } - } - - // client ip: - // node, nextjs: req.socket.remoteAddress - // express, koa: req.ip - // It may also be sent by proxies as specified in X-Forwarded-For or similar headers - if (include.ip) { - const ip = (req.headers && getClientIPAddress(req.headers)) || req.ip || (req.socket && req.socket.remoteAddress); - if (ip) { - event.user = { - ...event.user, - ip_address: ip, - }; - } - } - - return event; -} - -function extractQueryParams(req: PolymorphicRequest): string | Record | undefined { - // url (including path and query string): - // node, express: req.originalUrl - // koa, nextjs: req.url - let originalUrl = req.originalUrl || req.url || ''; - - if (!originalUrl) { - return; - } - - // The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and - // hostname on the beginning. Since the point here is just to grab the query string, it doesn't matter what we use. - if (originalUrl.startsWith('/')) { - originalUrl = `http://dogs.are.great${originalUrl}`; - } - - try { - const queryParams = req.query || new URL(originalUrl).search.slice(1); - return queryParams.length ? queryParams : undefined; - } catch { - return undefined; - } -} - -/** - * Transforms a `Headers` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into a simple key-value dict. - * The header keys will be lower case: e.g. A "Content-Type" header will be stored as "content-type". - */ -// TODO(v8): Make this function return undefined when the extraction fails. -export function winterCGHeadersToDict(winterCGHeaders: WebFetchHeaders): Record { - const headers: Record = {}; - try { - winterCGHeaders.forEach((value, key) => { - if (typeof value === 'string') { - // We check that value is a string even though it might be redundant to make sure prototype pollution is not possible. - headers[key] = value; - } - }); - } catch (e) { - DEBUG_BUILD && - logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.'); - } - - return headers; -} - -/** - * Convert common request headers to a simple dictionary. - */ -export function headersToDict(reqHeaders: Record): Record { - const headers: Record = Object.create(null); - - try { - Object.entries(reqHeaders).forEach(([key, value]) => { - if (typeof value === 'string') { - headers[key] = value; - } - }); - } catch (e) { - DEBUG_BUILD && - logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.'); - } - - return headers; -} - -/** - * Converts a `Request` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into the format that the `RequestData` integration understands. - */ -export function winterCGRequestToRequestData(req: WebFetchRequest): RequestEventData { - const headers = winterCGHeadersToDict(req.headers); - - return { - method: req.method, - url: req.url, - query_string: extractQueryParamsFromUrl(req.url), - headers, - // TODO: Can we extract body data from the request? - }; -} - -/** - * Convert a HTTP request object to RequestEventData to be passed as normalizedRequest. - * Instead of allowing `PolymorphicRequest` to be passed, - * we want to be more specific and generally require a http.IncomingMessage-like object. - */ -export function httpRequestToRequestData(request: { - method?: string; - url?: string; - headers?: { - [key: string]: string | string[] | undefined; - }; - protocol?: string; - socket?: unknown; -}): RequestEventData { - const headers = request.headers || {}; - const host = headers.host || ''; - const protocol = request.socket && (request.socket as { encrypted?: boolean }).encrypted ? 'https' : 'http'; - const originalUrl = request.url || ''; - const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`; - - // This is non-standard, but may be sometimes set - // It may be overwritten later by our own body handling - const data = (request as PolymorphicRequest).body || undefined; - - // This is non-standard, but may be set on e.g. Next.js or Express requests - const cookies = (request as PolymorphicRequest).cookies; - - return dropUndefinedKeys({ - url: absoluteUrl, - method: request.method, - query_string: extractQueryParamsFromUrl(originalUrl), - headers: headersToDict(headers), - cookies, - data, - }); -} - -/** Extract the query params from an URL. */ -export function extractQueryParamsFromUrl(url: string): string | undefined { - // url is path and query string - if (!url) { - return; - } - - try { - // The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and - // hostname as the base. Since the point here is just to grab the query string, it doesn't matter what we use. - const queryParams = new URL(url, 'http://dogs.are.great').search.slice(1); - return queryParams.length ? queryParams : undefined; - } catch { - return undefined; - } -} - -function extractNormalizedRequestData( - normalizedRequest: RequestEventData, - { include }: { include: string[] }, -): RequestEventData { - const includeKeys = include ? (Array.isArray(include) ? include : DEFAULT_REQUEST_INCLUDES) : []; - - const requestData: RequestEventData = {}; - const headers = { ...normalizedRequest.headers }; - - if (includeKeys.includes('headers')) { - requestData.headers = headers; - - // Remove the Cookie header in case cookie data should not be included in the event - if (!include.includes('cookies')) { - delete (headers as { cookie?: string }).cookie; - } - - // Remove IP headers in case IP data should not be included in the event - if (!include.includes('ip')) { - ipHeaderNames.forEach(ipHeaderName => { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete (headers as Record)[ipHeaderName]; - }); - } - } - - if (includeKeys.includes('method')) { - requestData.method = normalizedRequest.method; - } - - if (includeKeys.includes('url')) { - requestData.url = normalizedRequest.url; - } - - if (includeKeys.includes('cookies')) { - const cookies = normalizedRequest.cookies || (headers && headers.cookie ? parseCookie(headers.cookie) : undefined); - requestData.cookies = cookies || {}; - } - - if (includeKeys.includes('query_string')) { - requestData.query_string = normalizedRequest.query_string; - } - - if (includeKeys.includes('data')) { - requestData.data = normalizedRequest.data; - } - - return requestData; -} diff --git a/packages/core/src/utils-hoist/severity.ts b/packages/core/src/utils-hoist/severity.ts index cdf25f6104d0..8b20a03e7ac8 100644 --- a/packages/core/src/utils-hoist/severity.ts +++ b/packages/core/src/utils-hoist/severity.ts @@ -1,10 +1,5 @@ import type { SeverityLevel } from '../types-hoist'; -/** - * @deprecated This variable has been deprecated and will be removed in the next major version. - */ -export const validSeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug']; - /** * Converts a string-based level into a `SeverityLevel`, normalizing it along the way. * diff --git a/packages/core/src/utils-hoist/stacktrace.ts b/packages/core/src/utils-hoist/stacktrace.ts index f7713fb50744..5a1a6b33435f 100644 --- a/packages/core/src/utils-hoist/stacktrace.ts +++ b/packages/core/src/utils-hoist/stacktrace.ts @@ -97,10 +97,10 @@ export function stripSentryFramesAndReverse(stack: ReadonlyArray): S localStack.pop(); // When using synthetic events, we will have a 2 levels deep stack, as `new Error('Sentry syntheticException')` - // is produced within the hub itself, making it: + // is produced within the scope itself, making it: // // Sentry.captureException() - // getCurrentHub().captureException() + // scope.captureException() // // instead of just the top `Sentry` call itself. // This forces us to possibly strip an additional frame in the exact same was as above. diff --git a/packages/core/src/utils-hoist/supports.ts b/packages/core/src/utils-hoist/supports.ts index d1ae4b5b96d4..d52c06e702ea 100644 --- a/packages/core/src/utils-hoist/supports.ts +++ b/packages/core/src/utils-hoist/supports.ts @@ -6,8 +6,6 @@ const WINDOW = GLOBAL_OBJ as unknown as Window; declare const EdgeRuntime: string | undefined; -export { supportsHistory } from './vendor/supportsHistory'; - /** * Tells whether current environment supports ErrorEvent objects * {@link supportsErrorEvent}. @@ -56,6 +54,16 @@ export function supportsDOMException(): boolean { } } +/** + * Tells whether current environment supports History API + * {@link supportsHistory}. + * + * @returns Answer to the given question. + */ +export function supportsHistory(): boolean { + return 'history' in WINDOW; +} + /** * Tells whether current environment supports Fetch API * {@link supportsFetch}. @@ -116,7 +124,7 @@ export function supportsNativeFetch(): boolean { const sandbox = doc.createElement('iframe'); sandbox.hidden = true; doc.head.appendChild(sandbox); - if (sandbox.contentWindow && sandbox.contentWindow.fetch) { + if (sandbox.contentWindow?.fetch) { // eslint-disable-next-line @typescript-eslint/unbound-method result = isNativeFunction(sandbox.contentWindow.fetch); } diff --git a/packages/core/src/utils-hoist/syncpromise.ts b/packages/core/src/utils-hoist/syncpromise.ts index 015b76b39086..95aa45598727 100644 --- a/packages/core/src/utils-hoist/syncpromise.ts +++ b/packages/core/src/utils-hoist/syncpromise.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { isThenable } from './is'; @@ -40,29 +39,25 @@ export function rejectedSyncPromise(reason?: any): PromiseLike { }); } +type Executor = (resolve: (value?: T | PromiseLike | null) => void, reject: (reason?: any) => void) => void; + /** * Thenable class that behaves like a Promise and follows it's interface * but is not async internally */ -class SyncPromise implements PromiseLike { +export class SyncPromise implements PromiseLike { private _state: States; private _handlers: Array<[boolean, (value: T) => void, (reason: any) => any]>; private _value: any; - public constructor( - executor: (resolve: (value?: T | PromiseLike | null) => void, reject: (reason?: any) => void) => void, - ) { + public constructor(executor: Executor) { this._state = States.PENDING; this._handlers = []; - try { - executor(this._resolve, this._reject); - } catch (e) { - this._reject(e); - } + this._runExecutor(executor); } - /** JSDoc */ + /** @inheritdoc */ public then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null, @@ -99,14 +94,14 @@ class SyncPromise implements PromiseLike { }); } - /** JSDoc */ + /** @inheritdoc */ public catch( onrejected?: ((reason: any) => TResult | PromiseLike) | null, ): PromiseLike { return this.then(val => val, onrejected); } - /** JSDoc */ + /** @inheritdoc */ public finally(onfinally?: (() => void) | null): PromiseLike { return new SyncPromise((resolve, reject) => { let val: TResult | any; @@ -138,35 +133,8 @@ class SyncPromise implements PromiseLike { }); } - /** JSDoc */ - private readonly _resolve = (value?: T | PromiseLike | null) => { - this._setResult(States.RESOLVED, value); - }; - - /** JSDoc */ - private readonly _reject = (reason?: any) => { - this._setResult(States.REJECTED, reason); - }; - - /** JSDoc */ - private readonly _setResult = (state: States, value?: T | PromiseLike | any) => { - if (this._state !== States.PENDING) { - return; - } - - if (isThenable(value)) { - void (value as PromiseLike).then(this._resolve, this._reject); - return; - } - - this._state = state; - this._value = value; - - this._executeHandlers(); - }; - - /** JSDoc */ - private readonly _executeHandlers = () => { + /** Excute the resolve/reject handlers. */ + private _executeHandlers(): void { if (this._state === States.PENDING) { return; } @@ -189,7 +157,38 @@ class SyncPromise implements PromiseLike { handler[0] = true; }); - }; -} + } -export { SyncPromise }; + /** Run the executor for the SyncPromise. */ + private _runExecutor(executor: Executor): void { + const setResult = (state: States, value?: T | PromiseLike | any): void => { + if (this._state !== States.PENDING) { + return; + } + + if (isThenable(value)) { + void (value as PromiseLike).then(resolve, reject); + return; + } + + this._state = state; + this._value = value; + + this._executeHandlers(); + }; + + const resolve = (value: unknown): void => { + setResult(States.RESOLVED, value); + }; + + const reject = (reason: unknown): void => { + setResult(States.REJECTED, reason); + }; + + try { + executor(resolve, reject); + } catch (e) { + reject(e); + } + } +} diff --git a/packages/core/src/utils-hoist/time.ts b/packages/core/src/utils-hoist/time.ts index e03e7ba4d39b..c23915482590 100644 --- a/packages/core/src/utils-hoist/time.ts +++ b/packages/core/src/utils-hoist/time.ts @@ -19,8 +19,6 @@ interface Performance { /** * Returns a timestamp in seconds since the UNIX epoch using the Date API. - * - * TODO(v8): Return type should be rounded. */ export function dateTimestampInSeconds(): number { return Date.now() / ONE_SECOND_IN_MS; @@ -34,7 +32,7 @@ export function dateTimestampInSeconds(): number { */ function createUnixTimestampInSecondsFunc(): () => number { const { performance } = GLOBAL_OBJ as typeof GLOBAL_OBJ & { performance?: Performance }; - if (!performance || !performance.now) { + if (!performance?.now) { return dateTimestampInSeconds; } @@ -69,26 +67,21 @@ function createUnixTimestampInSecondsFunc(): () => number { export const timestampInSeconds = createUnixTimestampInSecondsFunc(); /** - * Internal helper to store what is the source of browserPerformanceTimeOrigin below. For debugging only. - * - * @deprecated This variable will be removed in the next major version. + * Cached result of getBrowserTimeOrigin. */ -export let _browserPerformanceTimeOriginMode: string; +let cachedTimeOrigin: [number | undefined, string] | undefined; /** - * The number of milliseconds since the UNIX epoch. This value is only usable in a browser, and only when the - * performance API is available. + * Gets the time origin and the mode used to determine it. */ -export const browserPerformanceTimeOrigin = ((): number | undefined => { +function getBrowserTimeOrigin(): [number | undefined, string] { // Unfortunately browsers may report an inaccurate time origin data, through either performance.timeOrigin or // performance.timing.navigationStart, which results in poor results in performance data. We only treat time origin // data as reliable if they are within a reasonable threshold of the current time. const { performance } = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; - if (!performance || !performance.now) { - // eslint-disable-next-line deprecation/deprecation - _browserPerformanceTimeOriginMode = 'none'; - return undefined; + if (!performance?.now) { + return [undefined, 'none']; } const threshold = 3600 * 1000; @@ -107,7 +100,7 @@ export const browserPerformanceTimeOrigin = ((): number | undefined => { // a valid fallback. In the absence of an initial time provided by the browser, fallback to the current time from the // Date API. // eslint-disable-next-line deprecation/deprecation - const navigationStart = performance.timing && performance.timing.navigationStart; + const navigationStart = performance.timing?.navigationStart; const hasNavigationStart = typeof navigationStart === 'number'; // if navigationStart isn't available set delta to threshold so it isn't used const navigationStartDelta = hasNavigationStart ? Math.abs(navigationStart + performanceNow - dateNow) : threshold; @@ -116,18 +109,24 @@ export const browserPerformanceTimeOrigin = ((): number | undefined => { if (timeOriginIsReliable || navigationStartIsReliable) { // Use the more reliable time origin if (timeOriginDelta <= navigationStartDelta) { - // eslint-disable-next-line deprecation/deprecation - _browserPerformanceTimeOriginMode = 'timeOrigin'; - return performance.timeOrigin; + return [performance.timeOrigin, 'timeOrigin']; } else { - // eslint-disable-next-line deprecation/deprecation - _browserPerformanceTimeOriginMode = 'navigationStart'; - return navigationStart; + return [navigationStart, 'navigationStart']; } } // Either both timeOrigin and navigationStart are skewed or neither is available, fallback to Date. - // eslint-disable-next-line deprecation/deprecation - _browserPerformanceTimeOriginMode = 'dateNow'; - return dateNow; -})(); + return [dateNow, 'dateNow']; +} + +/** + * The number of milliseconds since the UNIX epoch. This value is only usable in a browser, and only when the + * performance API is available. + */ +export function browserPerformanceTimeOrigin(): number | undefined { + if (!cachedTimeOrigin) { + cachedTimeOrigin = getBrowserTimeOrigin(); + } + + return cachedTimeOrigin[0]; +} diff --git a/packages/core/src/utils-hoist/tracing.ts b/packages/core/src/utils-hoist/tracing.ts index eb649ff34d6c..35b71fda472a 100644 --- a/packages/core/src/utils-hoist/tracing.ts +++ b/packages/core/src/utils-hoist/tracing.ts @@ -1,4 +1,5 @@ -import type { PropagationContext, TraceparentData } from '../types-hoist'; +import type { DynamicSamplingContext, PropagationContext, TraceparentData } from '../types-hoist'; +import { parseSampleRate } from '../utils/parseSampleRate'; import { baggageHeaderToDynamicSamplingContext } from './baggage'; import { generateSpanId, generateTraceId } from './propagationContext'; @@ -54,20 +55,28 @@ export function propagationContextFromHeaders( const traceparentData = extractTraceparentData(sentryTrace); const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggage); - if (!traceparentData || !traceparentData.traceId) { - return { traceId: generateTraceId(), spanId: generateSpanId() }; + if (!traceparentData?.traceId) { + return { + traceId: generateTraceId(), + sampleRand: Math.random(), + }; } - const { traceId, parentSpanId, parentSampled } = traceparentData; + const sampleRand = getSampleRandFromTraceparentAndDsc(traceparentData, dynamicSamplingContext); + + // The sample_rand on the DSC needs to be generated based on traceparent + baggage. + if (dynamicSamplingContext) { + dynamicSamplingContext.sample_rand = sampleRand.toString(); + } - const virtualSpanId = generateSpanId(); + const { traceId, parentSpanId, parentSampled } = traceparentData; return { traceId, parentSpanId, - spanId: virtualSpanId, sampled: parentSampled, dsc: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it + sampleRand, }; } @@ -75,8 +84,8 @@ export function propagationContextFromHeaders( * Create sentry-trace header from span context values. */ export function generateSentryTraceHeader( - traceId: string = generateTraceId(), - spanId: string = generateSpanId(), + traceId: string | undefined = generateTraceId(), + spanId: string | undefined = generateSpanId(), sampled?: boolean, ): string { let sampledString = ''; @@ -85,3 +94,32 @@ export function generateSentryTraceHeader( } return `${traceId}-${spanId}${sampledString}`; } + +/** + * Given any combination of an incoming trace, generate a sample rand based on its defined semantics. + * + * Read more: https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value + */ +function getSampleRandFromTraceparentAndDsc( + traceparentData: TraceparentData | undefined, + dsc: Partial | undefined, +): number { + // When there is an incoming sample rand use it. + const parsedSampleRand = parseSampleRate(dsc?.sample_rand); + if (parsedSampleRand !== undefined) { + return parsedSampleRand; + } + + // Otherwise, if there is an incoming sampling decision + sample rate, generate a sample rand that would lead to the same sampling decision. + const parsedSampleRate = parseSampleRate(dsc?.sample_rate); + if (parsedSampleRate && traceparentData?.parentSampled !== undefined) { + return traceparentData.parentSampled + ? // Returns a sample rand with positive sampling decision [0, sampleRate) + Math.random() * parsedSampleRate + : // Returns a sample rand with negative sampling decision [sampleRate, 1) + parsedSampleRate + Math.random() * (1 - parsedSampleRate); + } else { + // If nothing applies, return a random sample rand. + return Math.random(); + } +} diff --git a/packages/core/src/utils-hoist/url.ts b/packages/core/src/utils-hoist/url.ts index 44dc669da93a..e62d22f05e26 100644 --- a/packages/core/src/utils-hoist/url.ts +++ b/packages/core/src/utils-hoist/url.ts @@ -48,17 +48,6 @@ export function stripUrlQueryAndFragment(urlPath: string): string { return (urlPath.split(/[?#]/, 1) as [string, ...string[]])[0]; } -/** - * Returns number of URL segments of a passed string URL. - * - * @deprecated This function will be removed in the next major version. - */ -// TODO(v9): Hoist this function into the places where we use it. (as it stands only react router v6 instrumentation) -export function getNumberOfUrlSegments(url: string): number { - // split at '/' or at '\/' to split regex urls correctly - return url.split(/\\?\//).filter(s => s.length > 0 && s !== ',').length; -} - /** * Takes a URL object and returns a sanitized string which is safe to use as span name * see: https://develop.sentry.dev/sdk/data-handling/#structuring-data @@ -67,15 +56,13 @@ export function getSanitizedUrlString(url: PartialURL): string { const { protocol, host, path } = url; const filteredHost = - (host && - host - // Always filter out authority - .replace(/^.*@/, '[filtered]:[filtered]@') - // Don't show standard :80 (http) and :443 (https) ports to reduce the noise - // TODO: Use new URL global if it exists - .replace(/(:80)$/, '') - .replace(/(:443)$/, '')) || - ''; + host + // Always filter out authority + ?.replace(/^.*@/, '[filtered]:[filtered]@') + // Don't show standard :80 (http) and :443 (https) ports to reduce the noise + // TODO: Use new URL global if it exists + .replace(/(:80)$/, '') + .replace(/(:443)$/, '') || ''; return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`; } diff --git a/packages/core/src/utils-hoist/vendor/supportsHistory.ts b/packages/core/src/utils-hoist/vendor/supportsHistory.ts deleted file mode 100644 index bd0fbddec35f..000000000000 --- a/packages/core/src/utils-hoist/vendor/supportsHistory.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Based on https://github.com/angular/angular.js/pull/13945/files -// The MIT License - -// Copyright (c) 2010-2016 Google, Inc. http://angularjs.org - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import { GLOBAL_OBJ } from '../worldwide'; - -const WINDOW = GLOBAL_OBJ as unknown as Window; - -/** - * Tells whether current environment supports History API - * {@link supportsHistory}. - * - * @returns Answer to the given question. - */ -export function supportsHistory(): boolean { - // NOTE: in Chrome App environment, touching history.pushState, *even inside - // a try/catch block*, will cause Chrome to output an error to console.error - // borrowed from: https://github.com/angular/angular.js/pull/13945/files - // TODO(v9): Remove this custom check, it is pretty old and likely not needed anymore - const chromeVar = (WINDOW as { chrome?: { app?: { runtime?: unknown } } }).chrome; - const isChromePackagedApp = chromeVar && chromeVar.app && chromeVar.app.runtime; - const hasHistoryApi = 'history' in WINDOW && !!WINDOW.history.pushState && !!WINDOW.history.replaceState; - - return !isChromePackagedApp && hasHistoryApi; -} diff --git a/packages/core/src/utils-hoist/vercelWaitUntil.ts b/packages/core/src/utils-hoist/vercelWaitUntil.ts index 280130b766b7..f9bae863ce9a 100644 --- a/packages/core/src/utils-hoist/vercelWaitUntil.ts +++ b/packages/core/src/utils-hoist/vercelWaitUntil.ts @@ -1,9 +1,11 @@ import { GLOBAL_OBJ } from './worldwide'; interface VercelRequestContextGlobal { - get?(): { - waitUntil?: (task: Promise) => void; - }; + get?(): + | { + waitUntil?: (task: Promise) => void; + } + | undefined; } /** @@ -17,11 +19,9 @@ export function vercelWaitUntil(task: Promise): void { GLOBAL_OBJ[Symbol.for('@vercel/request-context')]; const ctx = - vercelRequestContextGlobal && vercelRequestContextGlobal.get && vercelRequestContextGlobal.get() - ? vercelRequestContextGlobal.get() - : {}; + vercelRequestContextGlobal?.get && vercelRequestContextGlobal.get() ? vercelRequestContextGlobal.get() : {}; - if (ctx && ctx.waitUntil) { + if (ctx?.waitUntil) { ctx.waitUntil(task); } } diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts index 92018731ff4f..426831038f13 100644 --- a/packages/core/src/utils-hoist/worldwide.ts +++ b/packages/core/src/utils-hoist/worldwide.ts @@ -12,44 +12,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Client, MetricsAggregator, Scope } from '../types-hoist'; - +import type { Carrier } from '../carrier'; import type { SdkSource } from './env'; -import type { logger } from './logger'; -import { SDK_VERSION } from './version'; - -interface SentryCarrier { - acs?: any; - stack?: any; - - globalScope?: Scope; - defaultIsolationScope?: Scope; - defaultCurrentScope?: Scope; - globalMetricsAggregators?: WeakMap | undefined; - logger?: typeof logger; - - /** Overwrites TextEncoder used in `@sentry/core`, need for `react-native@0.73` and older */ - encodePolyfill?: (input: string) => Uint8Array; - /** Overwrites TextDecoder used in `@sentry/core`, need for `react-native@0.73` and older */ - decodePolyfill?: (input: Uint8Array) => string; -} - -// TODO(v9): Clean up or remove this type -type BackwardsCompatibleSentryCarrier = SentryCarrier & { - // pre-v7 hub (replaced by .stack) - hub: any; - integrations?: any[]; - logger: any; - extensions?: { - /** Extension methods for the hub, which are bound to the current Hub instance */ - // eslint-disable-next-line @typescript-eslint/ban-types - [key: string]: Function; - }; -}; /** Internal global with common properties and Sentry extensions */ export type InternalGlobal = { - navigator?: { userAgent?: string }; + navigator?: { userAgent?: string; maxTouchPoints?: number }; console: Console; PerformanceObserver?: any; Sentry?: any; @@ -73,9 +41,6 @@ export type InternalGlobal = { * file. */ _sentryDebugIds?: Record; - __SENTRY__: Record, SentryCarrier> & { - version?: string; - } & BackwardsCompatibleSentryCarrier; /** * Raw module metadata that is injected by bundler plugins. * @@ -83,25 +48,7 @@ export type InternalGlobal = { */ _sentryModuleMetadata?: Record; _sentryEsmLoaderHookRegistered?: boolean; -}; +} & Carrier; /** Get's the global object for the current JavaScript runtime */ export const GLOBAL_OBJ = globalThis as unknown as InternalGlobal; - -/** - * Returns a global singleton contained in the global `__SENTRY__[]` object. - * - * If the singleton doesn't already exist in `__SENTRY__`, it will be created using the given factory - * function and added to the `__SENTRY__` object. - * - * @param name name of the global singleton on __SENTRY__ - * @param creator creator Factory function to create the singleton if it doesn't already exist on `__SENTRY__` - * @param obj (Optional) The global object on which to look for `__SENTRY__`, if not `GLOBAL_OBJ`'s return value - * @returns the singleton - */ -export function getGlobalSingleton(name: keyof SentryCarrier, creator: () => T, obj?: unknown): T { - const gbl = (obj || GLOBAL_OBJ) as InternalGlobal; - const __SENTRY__ = (gbl.__SENTRY__ = gbl.__SENTRY__ || {}); - const versionedCarrier = (__SENTRY__[SDK_VERSION] = __SENTRY__[SDK_VERSION] || {}); - return versionedCarrier[name] || (versionedCarrier[name] = creator()); -} diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index fccca13f87b9..93043b632c2d 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -1,5 +1,6 @@ +import type { ScopeData } from '../scope'; import { getDynamicSamplingContextFromSpan } from '../tracing/dynamicSamplingContext'; -import type { Breadcrumb, Event, ScopeData, Span } from '../types-hoist'; +import type { Breadcrumb, Event, Span } from '../types-hoist'; import { dropUndefinedKeys } from '../utils-hoist/object'; import { merge } from './merge'; import { getRootSpan, spanToJSON, spanToTraceContext } from './spanUtils'; @@ -113,22 +114,22 @@ function applyDataToEvent(event: Event, data: ScopeData): void { const { extra, tags, user, contexts, level, transactionName } = data; const cleanedExtra = dropUndefinedKeys(extra); - if (cleanedExtra && Object.keys(cleanedExtra).length) { + if (Object.keys(cleanedExtra).length) { event.extra = { ...cleanedExtra, ...event.extra }; } const cleanedTags = dropUndefinedKeys(tags); - if (cleanedTags && Object.keys(cleanedTags).length) { + if (Object.keys(cleanedTags).length) { event.tags = { ...cleanedTags, ...event.tags }; } const cleanedUser = dropUndefinedKeys(user); - if (cleanedUser && Object.keys(cleanedUser).length) { + if (Object.keys(cleanedUser).length) { event.user = { ...cleanedUser, ...event.user }; } const cleanedContexts = dropUndefinedKeys(contexts); - if (cleanedContexts && Object.keys(cleanedContexts).length) { + if (Object.keys(cleanedContexts).length) { event.contexts = { ...cleanedContexts, ...event.contexts }; } @@ -190,7 +191,7 @@ function applyFingerprintToEvent(event: Event, fingerprint: ScopeData['fingerpri } // If we have no data at all, remove empty array default - if (event.fingerprint && !event.fingerprint.length) { + if (!event.fingerprint.length) { delete event.fingerprint; } } diff --git a/packages/core/src/utils-hoist/cookie.ts b/packages/core/src/utils/cookie.ts similarity index 100% rename from packages/core/src/utils-hoist/cookie.ts rename to packages/core/src/utils/cookie.ts diff --git a/packages/core/src/utils/eventUtils.ts b/packages/core/src/utils/eventUtils.ts new file mode 100644 index 000000000000..716d12d2f4f8 --- /dev/null +++ b/packages/core/src/utils/eventUtils.ts @@ -0,0 +1,27 @@ +import type { Event } from '../types-hoist'; + +/** + * Get a list of possible event messages from a Sentry event. + */ +export function getPossibleEventMessages(event: Event): string[] { + const possibleMessages: string[] = []; + + if (event.message) { + possibleMessages.push(event.message); + } + + try { + // @ts-expect-error Try catching to save bundle size + const lastException = event.exception.values[event.exception.values.length - 1]; + if (lastException?.value) { + possibleMessages.push(lastException.value); + if (lastException.type) { + possibleMessages.push(`${lastException.type}: ${lastException.value}`); + } + } + } catch (e) { + // ignore errors here + } + + return possibleMessages; +} diff --git a/packages/core/src/utils/hasTracingEnabled.ts b/packages/core/src/utils/hasTracingEnabled.ts index 6d99eede931e..f00bf10ff367 100644 --- a/packages/core/src/utils/hasTracingEnabled.ts +++ b/packages/core/src/utils/hasTracingEnabled.ts @@ -10,14 +10,17 @@ declare const __SENTRY_TRACING__: boolean | undefined; * Tracing is enabled when at least one of `tracesSampleRate` and `tracesSampler` is defined in the SDK config. */ export function hasTracingEnabled( - maybeOptions?: Pick | undefined, + maybeOptions?: Pick | undefined, ): boolean { if (typeof __SENTRY_TRACING__ === 'boolean' && !__SENTRY_TRACING__) { return false; } const client = getClient(); - const options = maybeOptions || (client && client.getOptions()); - // eslint-disable-next-line deprecation/deprecation - return !!options && (options.enableTracing || 'tracesSampleRate' in options || 'tracesSampler' in options); + const options = maybeOptions || client?.getOptions(); + return ( + !!options && + // Note: This check is `!= null`, meaning "nullish" + (options.tracesSampleRate != null || !!options.tracesSampler) + ); } diff --git a/packages/core/src/utils/isSentryRequestUrl.ts b/packages/core/src/utils/isSentryRequestUrl.ts index 614c98bf4081..07fde7e29288 100644 --- a/packages/core/src/utils/isSentryRequestUrl.ts +++ b/packages/core/src/utils/isSentryRequestUrl.ts @@ -1,4 +1,5 @@ -import type { Client, DsnComponents } from '../types-hoist'; +import type { Client } from '../client'; +import type { DsnComponents } from '../types-hoist'; /** * Checks whether given url points to Sentry server @@ -6,8 +7,8 @@ import type { Client, DsnComponents } from '../types-hoist'; * @param url url to verify */ export function isSentryRequestUrl(url: string, client: Client | undefined): boolean { - const dsn = client && client.getDsn(); - const tunnel = client && client.getOptions().tunnel; + const dsn = client?.getDsn(); + const tunnel = client?.getOptions().tunnel; return checkDsn(url, dsn) || checkTunnel(url, tunnel); } diff --git a/packages/core/src/utils/merge.ts b/packages/core/src/utils/merge.ts index d80520b45cf6..25c386021adc 100644 --- a/packages/core/src/utils/merge.ts +++ b/packages/core/src/utils/merge.ts @@ -13,7 +13,7 @@ export function merge(initialObj: T, mergeObj: T, levels = 2): T { } // If the merge object is an empty object, and the initial object is not undefined, we return the initial object - if (initialObj && mergeObj && Object.keys(mergeObj).length === 0) { + if (initialObj && Object.keys(mergeObj).length === 0) { return initialObj; } diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index e896a401be20..e417640a387a 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -1,18 +1,10 @@ -import type { - CaptureContext, - Client, - ClientOptions, - Event, - EventHint, - Scope as ScopeInterface, - ScopeContext, - StackParser, -} from '../types-hoist'; - +import type { Client } from '../client'; import { DEFAULT_ENVIRONMENT } from '../constants'; import { getGlobalScope } from '../currentScopes'; import { notifyEventProcessors } from '../eventProcessors'; +import type { CaptureContext, ScopeContext } from '../scope'; import { Scope } from '../scope'; +import type { ClientOptions, Event, EventHint, StackParser } from '../types-hoist'; import { getFilenameToDebugIdMap } from '../utils-hoist/debug-ids'; import { addExceptionMechanism, uuid4 } from '../utils-hoist/misc'; import { normalize } from '../utils-hoist/normalize'; @@ -48,9 +40,9 @@ export function prepareEvent( options: ClientOptions, event: Event, hint: EventHint, - scope?: ScopeInterface, + scope?: Scope, client?: Client, - isolationScope?: ScopeInterface, + isolationScope?: Scope, ): PromiseLike { const { normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = options; const prepared: Event = { @@ -156,13 +148,13 @@ export function applyClientOptions(event: Event, options: ClientOptions): void { event.message = truncate(event.message, maxValueLength); } - const exception = event.exception && event.exception.values && event.exception.values[0]; - if (exception && exception.value) { + const exception = event.exception?.values?.[0]; + if (exception?.value) { exception.value = truncate(exception.value, maxValueLength); } const request = event.request; - if (request && request.url) { + if (request?.url) { request.url = truncate(request.url, maxValueLength); } } @@ -174,19 +166,13 @@ export function applyDebugIds(event: Event, stackParser: StackParser): void { // Build a map of filename -> debug_id const filenameDebugIdMap = getFilenameToDebugIdMap(stackParser); - try { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - event!.exception!.values!.forEach(exception => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - exception.stacktrace!.frames!.forEach(frame => { - if (filenameDebugIdMap && frame.filename) { - frame.debug_id = filenameDebugIdMap[frame.filename]; - } - }); + event.exception?.values?.forEach(exception => { + exception.stacktrace?.frames?.forEach(frame => { + if (frame.filename) { + frame.debug_id = filenameDebugIdMap[frame.filename]; + } }); - } catch (e) { - // To save bundle size we're just try catching here instead of checking for the existence of all the different objects. - } + }); } /** @@ -195,24 +181,18 @@ export function applyDebugIds(event: Event, stackParser: StackParser): void { export function applyDebugMeta(event: Event): void { // Extract debug IDs and filenames from the stack frames on the event. const filenameDebugIdMap: Record = {}; - try { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - event.exception!.values!.forEach(exception => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - exception.stacktrace!.frames!.forEach(frame => { - if (frame.debug_id) { - if (frame.abs_path) { - filenameDebugIdMap[frame.abs_path] = frame.debug_id; - } else if (frame.filename) { - filenameDebugIdMap[frame.filename] = frame.debug_id; - } - delete frame.debug_id; + event.exception?.values?.forEach(exception => { + exception.stacktrace?.frames?.forEach(frame => { + if (frame.debug_id) { + if (frame.abs_path) { + filenameDebugIdMap[frame.abs_path] = frame.debug_id; + } else if (frame.filename) { + filenameDebugIdMap[frame.filename] = frame.debug_id; } - }); + delete frame.debug_id; + } }); - } catch (e) { - // To save bundle size we're just try catching here instead of checking for the existence of all the different objects. - } + }); if (Object.keys(filenameDebugIdMap).length === 0) { return; @@ -285,7 +265,7 @@ function normalizeEvent(event: Event | null, depth: number, maxBreadth: number): // For now the decision is to skip normalization of Transactions and Spans, // so this block overwrites the normalized event to add back the original // Transaction information prior to normalization. - if (event.contexts && event.contexts.trace && normalized.contexts) { + if (event.contexts?.trace && normalized.contexts) { normalized.contexts.trace = event.contexts.trace; // event.contexts.trace.data may contain circular/dangerous data so we need to normalize it @@ -310,17 +290,14 @@ function normalizeEvent(event: Event | null, depth: number, maxBreadth: number): // flag integrations. It has a greater nesting depth than our other typed // Contexts, so we re-normalize with a fixed depth of 3 here. We do not want // to skip this in case of conflicting, user-provided context. - if (event.contexts && event.contexts.flags && normalized.contexts) { + if (event.contexts?.flags && normalized.contexts) { normalized.contexts.flags = normalize(event.contexts.flags, 3, maxBreadth); } return normalized; } -function getFinalScope( - scope: ScopeInterface | undefined, - captureContext: CaptureContext | undefined, -): ScopeInterface | undefined { +function getFinalScope(scope: Scope | undefined, captureContext: CaptureContext | undefined): Scope | undefined { if (!captureContext) { return scope; } @@ -355,9 +332,7 @@ export function parseEventHintOrCaptureContext( return hint; } -function hintIsScopeOrFunction( - hint: CaptureContext | EventHint, -): hint is ScopeInterface | ((scope: ScopeInterface) => ScopeInterface) { +function hintIsScopeOrFunction(hint: CaptureContext | EventHint): hint is Scope | ((scope: Scope) => Scope) { return hint instanceof Scope || typeof hint === 'function'; } @@ -369,7 +344,6 @@ const captureContextKeys: readonly ScopeContextProperty[] = [ 'contexts', 'tags', 'fingerprint', - 'requestSession', 'propagationContext', ] as const; diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts new file mode 100644 index 000000000000..039eff95d3b9 --- /dev/null +++ b/packages/core/src/utils/request.ts @@ -0,0 +1,135 @@ +import type { PolymorphicRequest, RequestEventData } from '../types-hoist'; +import type { WebFetchHeaders, WebFetchRequest } from '../types-hoist/webfetchapi'; +import { dropUndefinedKeys } from '../utils-hoist/object'; + +/** + * Transforms a `Headers` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into a simple key-value dict. + * The header keys will be lower case: e.g. A "Content-Type" header will be stored as "content-type". + */ +export function winterCGHeadersToDict(winterCGHeaders: WebFetchHeaders): Record { + const headers: Record = {}; + try { + winterCGHeaders.forEach((value, key) => { + if (typeof value === 'string') { + // We check that value is a string even though it might be redundant to make sure prototype pollution is not possible. + headers[key] = value; + } + }); + } catch { + // just return the empty headers + } + + return headers; +} + +/** + * Convert common request headers to a simple dictionary. + */ +export function headersToDict(reqHeaders: Record): Record { + const headers: Record = Object.create(null); + + try { + Object.entries(reqHeaders).forEach(([key, value]) => { + if (typeof value === 'string') { + headers[key] = value; + } + }); + } catch { + // just return the empty headers + } + + return headers; +} + +/** + * Converts a `Request` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into the format that the `RequestData` integration understands. + */ +export function winterCGRequestToRequestData(req: WebFetchRequest): RequestEventData { + const headers = winterCGHeadersToDict(req.headers); + + return { + method: req.method, + url: req.url, + query_string: extractQueryParamsFromUrl(req.url), + headers, + // TODO: Can we extract body data from the request? + }; +} + +/** + * Convert a HTTP request object to RequestEventData to be passed as normalizedRequest. + * Instead of allowing `PolymorphicRequest` to be passed, + * we want to be more specific and generally require a http.IncomingMessage-like object. + */ +export function httpRequestToRequestData(request: { + method?: string; + url?: string; + headers?: { + [key: string]: string | string[] | undefined; + }; + protocol?: string; + socket?: { + encrypted?: boolean; + remoteAddress?: string; + }; +}): RequestEventData { + const headers = request.headers || {}; + const host = typeof headers.host === 'string' ? headers.host : undefined; + const protocol = request.protocol || (request.socket?.encrypted ? 'https' : 'http'); + const url = request.url || ''; + + const absoluteUrl = getAbsoluteUrl({ + url, + host, + protocol, + }); + + // This is non-standard, but may be sometimes set + // It may be overwritten later by our own body handling + const data = (request as PolymorphicRequest).body || undefined; + + // This is non-standard, but may be set on e.g. Next.js or Express requests + const cookies = (request as PolymorphicRequest).cookies; + + return dropUndefinedKeys({ + url: absoluteUrl, + method: request.method, + query_string: extractQueryParamsFromUrl(url), + headers: headersToDict(headers), + cookies, + data, + }); +} + +function getAbsoluteUrl({ + url, + protocol, + host, +}: { url?: string; protocol: string; host?: string }): string | undefined { + if (url?.startsWith('http')) { + return url; + } + + if (url && host) { + return `${protocol}://${host}${url}`; + } + + return undefined; +} + +/** Extract the query params from an URL. */ +export function extractQueryParamsFromUrl(url: string): string | undefined { + // url is path and query string + if (!url) { + return; + } + + try { + // The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and + // hostname as the base. Since the point here is just to grab the query string, it doesn't matter what we use. + const queryParams = new URL(url, 'http://s.io').search.slice(1); + return queryParams.length ? queryParams : undefined; + } catch { + return undefined; + } +} diff --git a/packages/core/src/utils/spanOnScope.ts b/packages/core/src/utils/spanOnScope.ts index bdc47b66d208..33d0ff80dd12 100644 --- a/packages/core/src/utils/spanOnScope.ts +++ b/packages/core/src/utils/spanOnScope.ts @@ -1,4 +1,5 @@ -import type { Scope, Span } from '../types-hoist'; +import type { Scope } from '../scope'; +import type { Span } from '../types-hoist'; import { addNonEnumerableProperty } from '../utils-hoist/object'; const SCOPE_SPAN_FIELD = '_sentrySpan'; diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 594a297f9395..41435e8be373 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -1,14 +1,16 @@ import { getAsyncContextStrategy } from '../asyncContext'; import { getMainCarrier } from '../carrier'; import { getCurrentScope } from '../currentScopes'; -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 { + SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '../semanticAttributes'; import type { SentrySpan } from '../tracing/sentrySpan'; import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; +import { getCapturedScopesOnSpan } from '../tracing/utils'; import type { - MeasurementUnit, - Primitive, Span, SpanAttributes, SpanJSON, @@ -28,7 +30,6 @@ import { _getSpanForScope } from './spanOnScope'; export const TRACE_FLAG_NONE = 0x0; export const TRACE_FLAG_SAMPLED = 0x1; -// todo(v9): Remove this once we've stopped dropping spans via `beforeSendSpan` let hasShownSpanDropWarning = false; /** @@ -60,7 +61,9 @@ export function spanToTraceContext(span: Span): TraceContext { // If the span is remote, we use a random/virtual span as span_id to the trace context, // and the remote span as parent_span_id const parent_span_id = isRemote ? spanId : spanToJSON(span).parent_span_id; - const span_id = isRemote ? generateSpanId() : spanId; + const scope = getCapturedScopesOnSpan(span).scope; + + const span_id = isRemote ? scope?.getPropagationContext().propagationSpanId || generateSpanId() : spanId; return dropUndefinedKeys({ parent_span_id, @@ -112,46 +115,44 @@ function ensureTimestampInSeconds(timestamp: number): number { // Note: Because of this, we currently have a circular type dependency (which we opted out of in package.json). // This is not avoidable as we need `spanToJSON` in `spanUtils.ts`, which in turn is needed by `span.ts` for backwards compatibility. // And `spanToJSON` needs the Span class from `span.ts` to check here. -export function spanToJSON(span: Span): Partial { +export function spanToJSON(span: Span): SpanJSON { if (spanIsSentrySpan(span)) { return span.getSpanJSON(); } - 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), - }); - } + const { spanId: span_id, traceId: trace_id } = span.spanContext(); - // Finally, at least we have `spanContext()`.... - return { + // 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, - }; - } catch { - return {}; + 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, + }); } + + // Finally, at least we have `spanContext()`.... + // This should not actually happen in reality, but we need to handle it for type safety. + return { + span_id, + trace_id, + start_timestamp: 0, + data: {}, + }; } function spanIsOpenTelemetrySdkTraceBaseSpan(span: Span): span is OpenTelemetrySdkTraceBaseSpan { - const castSpan = span as OpenTelemetrySdkTraceBaseSpan; + const castSpan = span as Partial; return !!castSpan.attributes && !!castSpan.startTime && !!castSpan.name && !!castSpan.endTime && !!castSpan.status; } @@ -277,36 +278,41 @@ export function getActiveSpan(): Span | undefined { return _getSpanForScope(getCurrentScope()); } -/** - * 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); - } -} - /** * Logs a warning once if `beforeSendSpan` is used to drop spans. - * - * todo(v9): Remove this once we've stopped dropping spans via `beforeSendSpan`. */ export function showSpanDropWarning(): void { if (!hasShownSpanDropWarning) { consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( - '[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.', + '[Sentry] Returning null from `beforeSendSpan` is disallowed. To drop certain spans, configure the respective integrations directly.', ); }); hasShownSpanDropWarning = true; } } + +/** + * Updates the name of the given span and ensures that the span name is not + * overwritten by the Sentry SDK. + * + * Use this function instead of `span.updateName()` if you want to make sure that + * your name is kept. For some spans, for example root `http.server` spans the + * Sentry SDK would otherwise overwrite the span name with a high-quality name + * it infers when the span ends. + * + * Use this function in server code or when your span is started on the server + * and on the client (browser). If you only update a span name on the client, + * you can also use `span.updateName()` the SDK does not overwrite the name. + * + * @param span - The span to update the name of. + * @param name - The name to set on the span. + */ +export function updateSpanName(span: Span, name: string): void { + span.updateName(name); + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: name, + }); +} diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index b73ddf2828bf..56968a27c357 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -2,8 +2,9 @@ import { getAsyncContextStrategy } from '../asyncContext'; import { getMainCarrier } from '../carrier'; import { getClient, getCurrentScope } from '../currentScopes'; import { isEnabled } from '../exports'; +import type { Scope } from '../scope'; import { getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan } from '../tracing'; -import type { Scope, SerializedTraceData, Span } from '../types-hoist'; +import type { SerializedTraceData, Span } from '../types-hoist'; import { dynamicSamplingContextToSentryBaggageHeader } from '../utils-hoist/baggage'; import { logger } from '../utils-hoist/logger'; import { TRACEPARENT_REGEXP, generateSentryTraceHeader } from '../utils-hoist/tracing'; @@ -54,8 +55,6 @@ export function getTraceData(options: { span?: Span } = {}): SerializedTraceData * Get a sentry-trace header value for the given scope. */ function scopeToTraceHeader(scope: Scope): string { - // TODO(v9): Use generateSpanId() instead of spanId - // eslint-disable-next-line deprecation/deprecation - const { traceId, sampled, spanId } = scope.getPropagationContext(); - return generateSentryTraceHeader(traceId, spanId, sampled); + const { traceId, sampled, propagationSpanId } = scope.getPropagationContext(); + return generateSentryTraceHeader(traceId, propagationSpanId, sampled); } diff --git a/packages/core/src/utils/transactionEvent.ts b/packages/core/src/utils/transactionEvent.ts new file mode 100644 index 000000000000..9ec233b4f078 --- /dev/null +++ b/packages/core/src/utils/transactionEvent.ts @@ -0,0 +1,57 @@ +import { SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, SEMANTIC_ATTRIBUTE_PROFILE_ID } from '../semanticAttributes'; +import type { SpanJSON, TransactionEvent } from '../types-hoist'; +import { dropUndefinedKeys } from '../utils-hoist'; + +/** + * Converts a transaction event to a span JSON object. + */ +export function convertTransactionEventToSpanJson(event: TransactionEvent): SpanJSON { + const { trace_id, parent_span_id, span_id, status, origin, data, op } = event.contexts?.trace ?? {}; + + return dropUndefinedKeys({ + data: data ?? {}, + description: event.transaction, + op, + parent_span_id, + span_id: span_id ?? '', + start_timestamp: event.start_timestamp ?? 0, + status, + timestamp: event.timestamp, + trace_id: trace_id ?? '', + origin, + profile_id: data?.[SEMANTIC_ATTRIBUTE_PROFILE_ID] as string | undefined, + exclusive_time: data?.[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME] as number | undefined, + measurements: event.measurements, + is_segment: true, + }); +} + +/** + * Converts a span JSON object to a transaction event. + */ +export function convertSpanJsonToTransactionEvent(span: SpanJSON): TransactionEvent { + const event: TransactionEvent = { + type: 'transaction', + timestamp: span.timestamp, + start_timestamp: span.start_timestamp, + transaction: span.description, + contexts: { + trace: { + trace_id: span.trace_id, + span_id: span.span_id, + parent_span_id: span.parent_span_id, + op: span.op, + status: span.status, + origin: span.origin, + data: { + ...span.data, + ...(span.profile_id && { [SEMANTIC_ATTRIBUTE_PROFILE_ID]: span.profile_id }), + ...(span.exclusive_time && { [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: span.exclusive_time }), + }, + }, + }, + measurements: span.measurements, + }; + + return dropUndefinedKeys(event); +} diff --git a/packages/core/src/utils-hoist/vendor/getIpAddress.ts b/packages/core/src/vendor/getIpAddress.ts similarity index 98% rename from packages/core/src/utils-hoist/vendor/getIpAddress.ts rename to packages/core/src/vendor/getIpAddress.ts index 8b96fe2146af..da71a42f0778 100644 --- a/packages/core/src/utils-hoist/vendor/getIpAddress.ts +++ b/packages/core/src/vendor/getIpAddress.ts @@ -62,7 +62,7 @@ export function getClientIPAddress(headers: { [key: string]: string | string[] | return parseForwardedHeader(value); } - return value && value.split(',').map((v: string) => v.trim()); + return value?.split(',').map((v: string) => v.trim()); }); // Flatten the array and filter out any falsy entries diff --git a/packages/core/test/lib/baseclient.test.ts b/packages/core/test/lib/client.test.ts similarity index 93% rename from packages/core/test/lib/baseclient.test.ts rename to packages/core/test/lib/client.test.ts index ce480879bb27..19a10f7f509a 100644 --- a/packages/core/test/lib/baseclient.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -1,14 +1,9 @@ -import { SentryError, SyncPromise, dsnToString } from '@sentry/core'; -import type { Client, Envelope, ErrorEvent, Event, TransactionEvent } from '../../src/types-hoist'; - -import * as loggerModule from '../../src/utils-hoist/logger'; -import * as miscModule from '../../src/utils-hoist/misc'; -import * as stringModule from '../../src/utils-hoist/string'; -import * as timeModule from '../../src/utils-hoist/time'; - import { Scope, + SentryError, + SyncPromise, addBreadcrumb, + dsnToString, getCurrentScope, getIsolationScope, lastEventId, @@ -16,7 +11,13 @@ import { setCurrentClient, withMonitor, } from '../../src'; +import type { BaseClient, Client } from '../../src/client'; import * as integrationModule from '../../src/integration'; +import type { Envelope, ErrorEvent, Event, SpanJSON, TransactionEvent } from '../../src/types-hoist'; +import * as loggerModule from '../../src/utils-hoist/logger'; +import * as miscModule from '../../src/utils-hoist/misc'; +import * as stringModule from '../../src/utils-hoist/string'; +import * as timeModule from '../../src/utils-hoist/time'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; import { AdHocIntegration, TestIntegration } from '../mocks/integration'; import { makeFakeTransport } from '../mocks/transport'; @@ -34,7 +35,7 @@ jest.spyOn(loggerModule, 'consoleSandbox').mockImplementation(cb => cb()); jest.spyOn(stringModule, 'truncate').mockImplementation(str => str); jest.spyOn(timeModule, 'dateTimestampInSeconds').mockImplementation(() => 2020); -describe('BaseClient', () => { +describe('Client', () => { beforeEach(() => { TestClient.sendEventCalled = undefined; TestClient.instance = undefined; @@ -87,44 +88,6 @@ describe('BaseClient', () => { expect(consoleWarnSpy).toHaveBeenCalledTimes(0); consoleWarnSpy.mockRestore(); }); - - describe.each(['tracesSampleRate', 'tracesSampler', 'enableTracing'])('%s', key => { - it('warns when set to undefined', () => { - const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); - - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: undefined }); - new TestClient(options); - - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toBeCalledWith( - `[Sentry] Deprecation warning: \`${key}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`, - ); - consoleWarnSpy.mockRestore(); - }); - - it('warns when set to null', () => { - const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); - - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: null }); - new TestClient(options); - - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toBeCalledWith( - `[Sentry] Deprecation warning: \`${key}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`, - ); - consoleWarnSpy.mockRestore(); - }); - - it('does not warn when set to 0', () => { - const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); - - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: 0 }); - new TestClient(options); - - expect(consoleWarnSpy).toHaveBeenCalledTimes(0); - consoleWarnSpy.mockRestore(); - }); - }); }); describe('getOptions()', () => { @@ -366,6 +329,22 @@ describe('BaseClient', () => { // `captureException` should bail right away this second time around and not get as far as calling this again expect(clientEventFromException).toHaveBeenCalledTimes(1); }); + + test('captures logger message', () => { + const logSpy = jest.spyOn(loggerModule.logger, 'log').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + + client.captureException(new Error('test error here')); + client.captureException({}); + + expect(logSpy).toHaveBeenCalledTimes(2); + expect(logSpy).toBeCalledWith('Captured error event `test error here`'); + expect(logSpy).toBeCalledWith('Captured error event ``'); + + logSpy.mockRestore(); + }); }); describe('captureMessage', () => { @@ -442,6 +421,20 @@ describe('BaseClient', () => { }), ); }); + + test('captures logger message', () => { + const logSpy = jest.spyOn(loggerModule.logger, 'log').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + + client.captureMessage('test error here'); + + expect(logSpy).toHaveBeenCalledTimes(1); + expect(logSpy).toBeCalledWith('Captured error event `test error here`'); + + logSpy.mockRestore(); + }); }); describe('captureEvent() / prepareEvent()', () => { @@ -938,7 +931,6 @@ describe('BaseClient', () => { event_id: '972f45b826a248bba98e990878a177e1', spans: [ { - data: { _sentry_extra_metrics: { M1: { value: 1 }, M2: { value: 2 } } }, description: 'first-paint', timestamp: 1591603196.637835, op: 'paint', @@ -946,6 +938,7 @@ describe('BaseClient', () => { span_id: '9e15bf99fbe4bc80', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, }, { description: 'first-contentful-paint', @@ -955,6 +948,7 @@ describe('BaseClient', () => { span_id: 'aa554c1f506b0783', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, }, ], start_timestamp: 1591603196.614865, @@ -1001,14 +995,14 @@ describe('BaseClient', () => { }); test('calls `beforeSendSpan` and uses original spans without any changes', () => { - expect.assertions(2); + expect.assertions(3); const beforeSendSpan = jest.fn(span => span); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); const client = new TestClient(options); const transaction: Event = { - transaction: '/cats/are/great', + transaction: '/dogs/are/great', type: 'transaction', spans: [ { @@ -1016,36 +1010,94 @@ describe('BaseClient', () => { span_id: '9e15bf99fbe4bc80', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, }, { description: 'second span', span_id: 'aa554c1f506b0783', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, }, ], }; client.captureEvent(transaction); - expect(beforeSendSpan).toHaveBeenCalledTimes(2); + expect(beforeSendSpan).toHaveBeenCalledTimes(3); const capturedEvent = TestClient.instance!.event!; expect(capturedEvent.spans).toEqual(transaction.spans); + expect(capturedEvent.transaction).toEqual(transaction.transaction); }); - test('calls `beforeSend` and uses the modified event', () => { - expect.assertions(2); - - const beforeSend = jest.fn(event => { - event.message = 'changed1'; - return event; + test('does not modify existing contexts for root span in `beforeSendSpan`', () => { + const beforeSendSpan = jest.fn((span: SpanJSON) => { + return { + ...span, + data: { + modified: 'true', + }, + }; }); - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); const client = new TestClient(options); - client.captureEvent({ message: 'hello' }); + const transaction: Event = { + transaction: '/animals/are/great', + type: 'transaction', + spans: [], + breadcrumbs: [ + { + type: 'ui.click', + }, + ], + contexts: { + trace: { + data: { + modified: 'false', + dropMe: 'true', + }, + span_id: '9e15bf99fbe4bc80', + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + }, + app: { + data: { + modified: 'false', + }, + }, + }, + }; + client.captureEvent(transaction); - expect(beforeSend).toHaveBeenCalled(); - expect(TestClient.instance!.event!.message).toEqual('changed1'); + expect(beforeSendSpan).toHaveBeenCalledTimes(1); + const capturedEvent = TestClient.instance!.event!; + expect(capturedEvent).toEqual({ + transaction: '/animals/are/great', + breadcrumbs: [ + { + type: 'ui.click', + }, + ], + type: 'transaction', + spans: [], + environment: 'production', + event_id: '12312012123120121231201212312012', + start_timestamp: 0, + timestamp: 2020, + contexts: { + trace: { + data: { + modified: 'true', + }, + span_id: '9e15bf99fbe4bc80', + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + }, + app: { + data: { + modified: 'false', + }, + }, + }, + }); }); test('calls `beforeSendTransaction` and uses the modified event', () => { @@ -1076,9 +1128,9 @@ describe('BaseClient', () => { transaction: '/dogs/are/great', type: 'transaction', spans: [ - { span_id: 'span1', trace_id: 'trace1', start_timestamp: 1234 }, - { span_id: 'span2', trace_id: 'trace1', start_timestamp: 1234 }, - { span_id: 'span3', trace_id: 'trace1', start_timestamp: 1234 }, + { span_id: 'span1', trace_id: 'trace1', start_timestamp: 1234, data: {} }, + { span_id: 'span2', trace_id: 'trace1', start_timestamp: 1234, data: {} }, + { span_id: 'span3', trace_id: 'trace1', start_timestamp: 1234, data: {} }, ], }); @@ -1089,7 +1141,7 @@ describe('BaseClient', () => { }); test('calls `beforeSendSpan` and uses the modified spans', () => { - expect.assertions(3); + expect.assertions(4); const beforeSendSpan = jest.fn(span => { span.data = { version: 'bravo' }; @@ -1099,7 +1151,7 @@ describe('BaseClient', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); const client = new TestClient(options); const transaction: Event = { - transaction: '/cats/are/great', + transaction: '/dogs/are/great', type: 'transaction', spans: [ { @@ -1107,24 +1159,27 @@ describe('BaseClient', () => { span_id: '9e15bf99fbe4bc80', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, }, { description: 'second span', span_id: 'aa554c1f506b0783', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, }, ], }; client.captureEvent(transaction); - expect(beforeSendSpan).toHaveBeenCalledTimes(2); + expect(beforeSendSpan).toHaveBeenCalledTimes(3); const capturedEvent = TestClient.instance!.event!; for (const [idx, span] of capturedEvent.spans!.entries()) { const originalSpan = transaction.spans![idx]; expect(span).toEqual({ ...originalSpan, data: { version: 'bravo' } }); } + expect(capturedEvent.contexts?.trace?.data).toEqual({ version: 'bravo' }); }); test('calls `beforeSend` and discards the event', () => { @@ -1165,15 +1220,15 @@ describe('BaseClient', () => { expect(loggerWarnSpy).toBeCalledWith('before send for type `transaction` returned `null`, will not send event.'); }); - test('calls `beforeSendSpan` and discards the span', () => { + test('does not discard span and warn when returning null from `beforeSendSpan', () => { const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); - const beforeSendSpan = jest.fn(() => null); + const beforeSendSpan = jest.fn(() => null as unknown as SpanJSON); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); const client = new TestClient(options); const transaction: Event = { - transaction: '/cats/are/great', + transaction: '/dogs/are/great', type: 'transaction', spans: [ { @@ -1181,25 +1236,27 @@ describe('BaseClient', () => { span_id: '9e15bf99fbe4bc80', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, }, { description: 'second span', span_id: 'aa554c1f506b0783', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, }, ], }; client.captureEvent(transaction); - expect(beforeSendSpan).toHaveBeenCalledTimes(2); + expect(beforeSendSpan).toHaveBeenCalledTimes(3); const capturedEvent = TestClient.instance!.event!; - expect(capturedEvent.spans).toHaveLength(0); - expect(client['_outcomes']).toEqual({ 'before_send:span': 2 }); + expect(capturedEvent.spans).toHaveLength(2); + expect(client['_outcomes']).toEqual({}); expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toBeCalledWith( - '[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.', + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[Sentry] Returning null from `beforeSendSpan` is disallowed. To drop certain spans, configure the respective integrations directly.', ); consoleWarnSpy.mockRestore(); }); @@ -1460,9 +1517,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }, {}); expect(beforeSend).toHaveBeenCalled(); - expect(recordLostEventSpy).toHaveBeenCalledWith('before_send', 'error', { - message: 'hello', - }); + expect(recordLostEventSpy).toHaveBeenCalledWith('before_send', 'error'); }); test('`beforeSendTransaction` records dropped events', () => { @@ -1482,10 +1537,7 @@ describe('BaseClient', () => { client.captureEvent({ transaction: '/dogs/are/great', type: 'transaction' }); expect(beforeSendTransaction).toHaveBeenCalled(); - expect(recordLostEventSpy).toHaveBeenCalledWith('before_send', 'transaction', { - transaction: '/dogs/are/great', - type: 'transaction', - }); + expect(recordLostEventSpy).toHaveBeenCalledWith('before_send', 'transaction'); }); test('event processor drops error event when it returns `null`', () => { @@ -1537,9 +1589,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }, {}, scope); - expect(recordLostEventSpy).toHaveBeenCalledWith('event_processor', 'error', { - message: 'hello', - }); + expect(recordLostEventSpy).toHaveBeenCalledWith('event_processor', 'error'); }); test('event processor records dropped transaction events', () => { @@ -1555,10 +1605,7 @@ describe('BaseClient', () => { client.captureEvent({ transaction: '/dogs/are/great', type: 'transaction' }, {}, scope); - expect(recordLostEventSpy).toHaveBeenCalledWith('event_processor', 'transaction', { - transaction: '/dogs/are/great', - type: 'transaction', - }); + expect(recordLostEventSpy).toHaveBeenCalledWith('event_processor', 'transaction'); }); test('mutating transaction name with event processors sets transaction-name-change metadata', () => { @@ -1647,9 +1694,23 @@ describe('BaseClient', () => { const recordLostEventSpy = jest.spyOn(client, 'recordDroppedEvent'); client.captureEvent({ message: 'hello' }, {}); - expect(recordLostEventSpy).toHaveBeenCalledWith('sample_rate', 'error', { - message: 'hello', - }); + expect(recordLostEventSpy).toHaveBeenCalledWith('sample_rate', 'error'); + }); + + test('captures logger message', () => { + const logSpy = jest.spyOn(loggerModule.logger, 'log').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + + client.captureEvent({ message: 'hello' }); + // transactions are ignored and not logged + client.captureEvent({ type: 'transaction', message: 'hello 2' }); + + expect(logSpy).toHaveBeenCalledTimes(1); + expect(logSpy).toBeCalledWith('Captured error event `hello`'); + + logSpy.mockRestore(); }); }); @@ -2044,7 +2105,8 @@ describe('BaseClient', () => { // Make sure types work for both Client & BaseClient const scenarios = [ - ['BaseClient', new TestClient(options)], + // eslint-disable-next-line deprecation/deprecation + ['BaseClient', new TestClient(options) as BaseClient], ['Client', new TestClient(options) as Client], ] as const; diff --git a/packages/core/test/lib/envelope.test.ts b/packages/core/test/lib/envelope.test.ts index 8909fb8df202..81e299ada752 100644 --- a/packages/core/test/lib/envelope.test.ts +++ b/packages/core/test/lib/envelope.test.ts @@ -1,5 +1,4 @@ -import type { Client, DsnComponents, DynamicSamplingContext, Event } from '../../src/types-hoist'; - +import type { Client } from '../../src'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SentrySpan, @@ -9,6 +8,7 @@ import { setCurrentClient, } from '../../src'; import { createEventEnvelope, createSpanEnvelope } from '../../src/envelope'; +import type { DsnComponents, DynamicSamplingContext, Event } from '../../src/types-hoist'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; const testDsn: DsnComponents = { protocol: 'https', projectId: 'abc', host: 'testry.io', publicKey: 'pubKey123' }; @@ -116,7 +116,7 @@ describe('createSpanEnvelope', () => { const spanEnvelope = createSpanEnvelope([span]); - const spanItem = spanEnvelope[1]?.[0]?.[1]; + const spanItem = spanEnvelope[1][0]?.[1]; expect(spanItem).toEqual({ data: { 'sentry.origin': 'manual', @@ -207,7 +207,7 @@ describe('createSpanEnvelope', () => { expect(beforeSendSpan).toHaveBeenCalled(); - const spanItem = spanEnvelope[1]?.[0]?.[1]; + const spanItem = spanEnvelope[1][0]?.[1]; expect(spanItem).toEqual({ data: { 'sentry.origin': 'manual', @@ -242,7 +242,7 @@ describe('createSpanEnvelope', () => { expect(beforeSendSpan).toHaveBeenCalled(); - const spanItem = spanEnvelope[1]?.[0]?.[1]; + const spanItem = spanEnvelope[1][0]?.[1]; expect(spanItem).toEqual({ data: { 'sentry.origin': 'manual', diff --git a/packages/core/test/lib/feedback.test.ts b/packages/core/test/lib/feedback.test.ts index 0146c834d6c1..717329d0b0e0 100644 --- a/packages/core/test/lib/feedback.test.ts +++ b/packages/core/test/lib/feedback.test.ts @@ -260,8 +260,9 @@ describe('captureFeedback', () => { getCurrentScope().setPropagationContext({ traceId, - spanId, + parentSpanId: spanId, dsc, + sampleRand: 0.42, }); const eventId = captureFeedback({ @@ -290,7 +291,8 @@ describe('captureFeedback', () => { contexts: { trace: { trace_id: traceId, - span_id: spanId, + parent_span_id: spanId, + span_id: expect.stringMatching(/[a-f0-9]{16}/), }, feedback: { message: 'test', @@ -312,7 +314,7 @@ describe('captureFeedback', () => { getDefaultTestClientOptions({ dsn: 'https://dsn@ingest.f00.f00/1', enableSend: true, - enableTracing: true, + tracesSampleRate: 1, // We don't care about transactions here... beforeSendTransaction() { return null; @@ -350,6 +352,7 @@ describe('captureFeedback', () => { sampled: 'true', sample_rate: '1', transaction: 'test-span', + sample_rand: expect.any(String), }, }, [ @@ -382,7 +385,7 @@ describe('captureFeedback', () => { getDefaultTestClientOptions({ dsn: 'https://dsn@ingest.f00.f00/1', enableSend: true, - enableTracing: true, + tracesSampleRate: 1, // We don't care about transactions here... beforeSendTransaction() { return null; diff --git a/packages/core/test/lib/hint.test.ts b/packages/core/test/lib/hint.test.ts index d455a5bd5e44..f7fd5ff83ae4 100644 --- a/packages/core/test/lib/hint.test.ts +++ b/packages/core/test/lib/hint.test.ts @@ -14,7 +14,6 @@ describe('Hint', () => { afterEach(() => { jest.clearAllMocks(); - // @ts-expect-error for testing delete GLOBAL_OBJ.__SENTRY__; }); diff --git a/packages/core/test/lib/integrations/captureconsole.test.ts b/packages/core/test/lib/integrations/captureconsole.test.ts index 4d480757fff1..cea4075f4d5e 100644 --- a/packages/core/test/lib/integrations/captureconsole.test.ts +++ b/packages/core/test/lib/integrations/captureconsole.test.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/unbound-method */ +import type { Client } from '../../../src'; import * as CurrentScopes from '../../../src/currentScopes'; import * as SentryCore from '../../../src/exports'; -import type { Client, ConsoleLevel, Event } from '../../../src/types-hoist'; - import { captureConsoleIntegration } from '../../../src/integrations/captureconsole'; +import type { ConsoleLevel, Event } from '../../../src/types-hoist'; import { addConsoleInstrumentationHandler } from '../../../src/utils-hoist/instrument/console'; import { resetInstrumentationHandlers } from '../../../src/utils-hoist/instrument/handlers'; import { CONSOLE_LEVELS, originalConsoleMethods } from '../../../src/utils-hoist/logger'; @@ -306,8 +306,7 @@ describe('CaptureConsole setup', () => { }); describe('exception mechanism', () => { - // TODO (v9): Flip this below after adjusting the default value for `handled` in the integration - it("marks captured exception's mechanism as unhandled by default", () => { + it("marks captured exception's mechanism as handled by default", () => { const captureConsole = captureConsoleIntegration({ levels: ['error'] }); captureConsole.setup?.(mockClient); @@ -326,7 +325,7 @@ describe('CaptureConsole setup', () => { expect(mockScope.addEventProcessor).toHaveBeenCalledTimes(1); expect(someEvent.exception?.values?.[0]?.mechanism).toEqual({ - handled: false, + handled: true, type: 'console', }); }); diff --git a/packages/core/test/lib/integrations/debug.test.ts b/packages/core/test/lib/integrations/debug.test.ts deleted file mode 100644 index 00a938a185e6..000000000000 --- a/packages/core/test/lib/integrations/debug.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { Client, Event, EventHint } from '../../../src/types-hoist'; - -import { debugIntegration } from '../../../src/integrations/debug'; - -function testEventLogged( - // eslint-disable-next-line deprecation/deprecation - integration: ReturnType, - testEvent?: Event, - testEventHint?: EventHint, -) { - const callbacks: ((event: Event, hint?: EventHint) => void)[] = []; - - const client: Client = { - on(hook: string, callback: (event: Event, hint?: EventHint) => void) { - expect(hook).toEqual('beforeSendEvent'); - callbacks.push(callback); - }, - } as Client; - - integration.setup?.(client); - - expect(callbacks.length).toEqual(1); - - if (testEvent) { - callbacks[0]?.(testEvent, testEventHint); - } -} - -// Replace console log with a mock so we can check for invocations -const mockConsoleLog = jest.fn(); -// eslint-disable-next-line @typescript-eslint/unbound-method -const originalConsoleLog = global.console.log; -global.console.log = mockConsoleLog; - -describe('Debug integration setup should register an event processor that', () => { - afterAll(() => { - // Reset mocked console log to original one - global.console.log = originalConsoleLog; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('logs an event', () => { - // eslint-disable-next-line deprecation/deprecation - const debug = debugIntegration(); - const testEvent = { event_id: 'some event' }; - - testEventLogged(debug, testEvent); - - expect(mockConsoleLog).toHaveBeenCalledTimes(1); - expect(mockConsoleLog).toBeCalledWith(testEvent); - }); - - it('logs an event hint if available', () => { - // eslint-disable-next-line deprecation/deprecation - const debug = debugIntegration(); - - const testEvent = { event_id: 'some event' }; - const testEventHint = { event_id: 'some event hint' }; - - testEventLogged(debug, testEvent, testEventHint); - - expect(mockConsoleLog).toHaveBeenCalledTimes(2); - expect(mockConsoleLog).toBeCalledWith(testEvent); - expect(mockConsoleLog).toBeCalledWith(testEventHint); - }); - - it('logs events in stringified format when `stringify` option was set', () => { - // eslint-disable-next-line deprecation/deprecation - const debug = debugIntegration({ stringify: true }); - const testEvent = { event_id: 'some event' }; - - testEventLogged(debug, testEvent); - - expect(mockConsoleLog).toHaveBeenCalledTimes(1); - expect(mockConsoleLog).toBeCalledWith(JSON.stringify(testEvent, null, 2)); - }); - - it('logs event hints in stringified format when `stringify` option was set', () => { - // eslint-disable-next-line deprecation/deprecation - const debug = debugIntegration({ stringify: true }); - - const testEvent = { event_id: 'some event' }; - const testEventHint = { event_id: 'some event hint' }; - - testEventLogged(debug, testEvent, testEventHint); - - expect(mockConsoleLog).toHaveBeenCalledTimes(2); - expect(mockConsoleLog).toBeCalledWith(JSON.stringify(testEventHint, null, 2)); - }); -}); diff --git a/packages/core/test/lib/integrations/requestdata.test.ts b/packages/core/test/lib/integrations/requestdata.test.ts deleted file mode 100644 index aebd140b2bf3..000000000000 --- a/packages/core/test/lib/integrations/requestdata.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { IncomingMessage } from 'http'; -import type { RequestDataIntegrationOptions } from '../../../src'; -import { requestDataIntegration, setCurrentClient } from '../../../src'; -import type { Event, EventProcessor } from '../../../src/types-hoist'; - -import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; - -import * as requestDataModule from '../../../src/utils-hoist/requestdata'; - -const addRequestDataToEventSpy = jest.spyOn(requestDataModule, 'addRequestDataToEvent'); - -const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; -const method = 'wagging'; -const protocol = 'mutualsniffing'; -const hostname = 'the.dog.park'; -const path = '/by/the/trees/'; -const queryString = 'chase=me&please=thankyou'; - -function initWithRequestDataIntegrationOptions(integrationOptions: RequestDataIntegrationOptions): EventProcessor { - const integration = requestDataIntegration({ - ...integrationOptions, - }); - - const client = new TestClient( - getDefaultTestClientOptions({ - dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', - integrations: [integration], - }), - ); - - setCurrentClient(client); - client.init(); - - const eventProcessors = client['_eventProcessors'] as EventProcessor[]; - const eventProcessor = eventProcessors.find(processor => processor.id === 'RequestData'); - - expect(eventProcessor).toBeDefined(); - - return eventProcessor!; -} - -describe('`RequestData` integration', () => { - let req: IncomingMessage, event: Event; - - beforeEach(() => { - req = { - headers, - method, - protocol, - hostname, - originalUrl: `${path}?${queryString}`, - } as unknown as IncomingMessage; - event = { sdkProcessingMetadata: { request: req } }; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('option conversion', () => { - it('leaves `ip` and `user` at top level of `include`', () => { - const requestDataEventProcessor = initWithRequestDataIntegrationOptions({ include: { ip: false, user: true } }); - - void requestDataEventProcessor(event, {}); - - const passedOptions = addRequestDataToEventSpy.mock.calls[0]?.[2]; - - expect(passedOptions?.include).toEqual(expect.objectContaining({ ip: false, user: true })); - }); - - it('moves `transactionNamingScheme` to `transaction` include', () => { - const requestDataEventProcessor = initWithRequestDataIntegrationOptions({ transactionNamingScheme: 'path' }); - - void requestDataEventProcessor(event, {}); - - const passedOptions = addRequestDataToEventSpy.mock.calls[0]?.[2]; - - expect(passedOptions?.include).toEqual(expect.objectContaining({ transaction: 'path' })); - }); - - it('moves `true` request keys into `request` include, but omits `false` ones', async () => { - const requestDataEventProcessor = initWithRequestDataIntegrationOptions({ - include: { data: true, cookies: false }, - }); - - void requestDataEventProcessor(event, {}); - - const passedOptions = addRequestDataToEventSpy.mock.calls[0]?.[2]; - - expect(passedOptions?.include?.request).toEqual(expect.arrayContaining(['data'])); - expect(passedOptions?.include?.request).not.toEqual(expect.arrayContaining(['cookies'])); - }); - - it('moves `true` user keys into `user` include, but omits `false` ones', async () => { - const requestDataEventProcessor = initWithRequestDataIntegrationOptions({ - include: { user: { id: true, email: false } }, - }); - - void requestDataEventProcessor(event, {}); - - const passedOptions = addRequestDataToEventSpy.mock.calls[0]?.[2]; - - expect(passedOptions?.include?.user).toEqual(expect.arrayContaining(['id'])); - expect(passedOptions?.include?.user).not.toEqual(expect.arrayContaining(['email'])); - }); - }); -}); diff --git a/packages/core/test/lib/integrations/sessiontiming.test.ts b/packages/core/test/lib/integrations/sessiontiming.test.ts deleted file mode 100644 index fb694fe8be0f..000000000000 --- a/packages/core/test/lib/integrations/sessiontiming.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { sessionTimingIntegration } from '../../../src/integrations/sessiontiming'; -import type { Event } from '../../../src/types-hoist'; - -// eslint-disable-next-line deprecation/deprecation -const sessionTiming = sessionTimingIntegration(); - -describe('SessionTiming', () => { - it('should work as expected', () => { - const event = sessionTiming.processEvent?.( - { - extra: { - some: 'value', - }, - }, - {}, - {} as any, - ) as Event; - - expect(typeof event.extra?.['session:start']).toBe('number'); - expect(typeof event.extra?.['session:duration']).toBe('number'); - expect(typeof event.extra?.['session:end']).toBe('number'); - expect(event.extra?.some).toEqual('value'); - }); -}); diff --git a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts index ec35cf07f6ab..9679cfe2b474 100644 --- a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts +++ b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts @@ -1,6 +1,7 @@ +import type { Client } from '../../../src/client'; import { thirdPartyErrorFilterIntegration } from '../../../src/integrations/third-party-errors-filter'; import { addMetadataToStackFrames } from '../../../src/metadata'; -import type { Client, Event } from '../../../src/types-hoist'; +import type { Event } from '../../../src/types-hoist'; import { nodeStackLineParser } from '../../../src/utils-hoist/node-stack-trace'; import { createStackParser } from '../../../src/utils-hoist/stacktrace'; import { GLOBAL_OBJ } from '../../../src/utils-hoist/worldwide'; diff --git a/packages/core/test/lib/integrations/zoderrrors.test.ts b/packages/core/test/lib/integrations/zoderrrors.test.ts index d5583fb57380..924ee5dd27da 100644 --- a/packages/core/test/lib/integrations/zoderrrors.test.ts +++ b/packages/core/test/lib/integrations/zoderrrors.test.ts @@ -20,11 +20,7 @@ class ZodError extends Error { super(); const actualProto = new.target.prototype; - if (Object.setPrototypeOf) { - Object.setPrototypeOf(this, actualProto); - } else { - (this as any).__proto__ = actualProto; - } + Object.setPrototypeOf(this, actualProto); this.name = 'ZodError'; this.issues = issues; diff --git a/packages/core/test/lib/metrics/aggregator.test.ts b/packages/core/test/lib/metrics/aggregator.test.ts deleted file mode 100644 index 2a471d12bb04..000000000000 --- a/packages/core/test/lib/metrics/aggregator.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { MetricsAggregator } from '../../../src/metrics/aggregator'; -import { MAX_WEIGHT } from '../../../src/metrics/constants'; -import { CounterMetric } from '../../../src/metrics/instance'; -import { serializeMetricBuckets } from '../../../src/metrics/utils'; -import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; - -let testClient: TestClient; - -describe('MetricsAggregator', () => { - const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); - - beforeEach(() => { - jest.useFakeTimers('legacy'); - testClient = new TestClient(options); - }); - - it('adds items to buckets', () => { - const aggregator = new MetricsAggregator(testClient); - aggregator.add('c', 'requests', 1); - expect(aggregator['_buckets'].size).toEqual(1); - - const firstValue = aggregator['_buckets'].values().next().value; - expect(firstValue).toEqual({ - metric: expect.any(CounterMetric), - metricType: 'c', - name: 'requests', - tags: {}, - timestamp: expect.any(Number), - unit: 'none', - }); - }); - - it('groups same items together', () => { - const aggregator = new MetricsAggregator(testClient); - aggregator.add('c', 'requests', 1); - expect(aggregator['_buckets'].size).toEqual(1); - aggregator.add('c', 'requests', 1); - expect(aggregator['_buckets'].size).toEqual(1); - - const firstValue = aggregator['_buckets'].values().next().value; - expect(firstValue).toEqual({ - metric: expect.any(CounterMetric), - metricType: 'c', - name: 'requests', - tags: {}, - timestamp: expect.any(Number), - unit: 'none', - }); - expect(firstValue.metric._value).toEqual(2); - }); - - it('differentiates based on tag value', () => { - const aggregator = new MetricsAggregator(testClient); - aggregator.add('g', 'cpu', 50); - expect(aggregator['_buckets'].size).toEqual(1); - aggregator.add('g', 'cpu', 55, undefined, { a: 'value' }); - expect(aggregator['_buckets'].size).toEqual(2); - }); - - describe('serializeBuckets', () => { - it('serializes ', () => { - const aggregator = new MetricsAggregator(testClient); - aggregator.add('c', 'requests', 8); - aggregator.add('g', 'cpu', 50); - aggregator.add('g', 'cpu', 55); - aggregator.add('g', 'cpu', 52); - aggregator.add('d', 'lcp', 1, 'second', { a: 'value', b: 'anothervalue' }); - aggregator.add('d', 'lcp', 1.2, 'second', { a: 'value', b: 'anothervalue' }); - aggregator.add('s', 'important_people', 'a', 'none', { numericKey: 2 }); - aggregator.add('s', 'important_people', 'b', 'none', { numericKey: 2 }); - - const metricBuckets = Array.from(aggregator['_buckets']).map(([, bucketItem]) => bucketItem); - const serializedBuckets = serializeMetricBuckets(metricBuckets); - - expect(serializedBuckets).toContain('requests@none:8|c|T'); - expect(serializedBuckets).toContain('cpu@none:52:50:55:157:3|g|T'); - expect(serializedBuckets).toContain('lcp@second:1:1.2|d|#a:value,b:anothervalue|T'); - expect(serializedBuckets).toContain('important_people@none:97:98|s|#numericKey:2|T'); - }); - }); - - describe('close', () => { - test('should flush immediately', () => { - const capture = jest.spyOn(testClient, 'sendEnvelope'); - const aggregator = new MetricsAggregator(testClient); - aggregator.add('c', 'requests', 1); - aggregator.close(); - // It should clear the interval. - expect(clearInterval).toHaveBeenCalled(); - expect(capture).toBeCalled(); - expect(capture).toBeCalledTimes(1); - }); - }); - - describe('flush', () => { - test('should flush immediately', () => { - const capture = jest.spyOn(testClient, 'sendEnvelope'); - const aggregator = new MetricsAggregator(testClient); - aggregator.add('c', 'requests', 1); - aggregator.flush(); - expect(capture).toBeCalled(); - expect(capture).toBeCalledTimes(1); - - capture.mockReset(); - aggregator.close(); - // It should clear the interval. - expect(clearInterval).toHaveBeenCalled(); - - // It shouldn't be called since it's been already flushed. - expect(capture).toBeCalledTimes(0); - }); - - test('should not capture if empty', () => { - const capture = jest.spyOn(testClient, 'sendEnvelope'); - const aggregator = new MetricsAggregator(testClient); - aggregator.add('c', 'requests', 1); - aggregator.flush(); - expect(capture).toBeCalledTimes(1); - capture.mockReset(); - aggregator.close(); - expect(capture).toBeCalledTimes(0); - }); - }); - - describe('add', () => { - test('it should respect the max weight and flush if exceeded', () => { - const capture = jest.spyOn(testClient, 'sendEnvelope'); - const aggregator = new MetricsAggregator(testClient); - - for (let i = 0; i < MAX_WEIGHT; i++) { - aggregator.add('c', 'requests', 1); - } - - expect(capture).toBeCalledTimes(1); - aggregator.close(); - }); - }); -}); diff --git a/packages/core/test/lib/metrics/browser-aggregator.test.ts b/packages/core/test/lib/metrics/browser-aggregator.test.ts deleted file mode 100644 index e5ed6b3f8296..000000000000 --- a/packages/core/test/lib/metrics/browser-aggregator.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { BrowserMetricsAggregator } from '../../../src/metrics/browser-aggregator'; -import { CounterMetric } from '../../../src/metrics/instance'; -import { serializeMetricBuckets } from '../../../src/metrics/utils'; -import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; - -function _cleanupAggregator(aggregator: BrowserMetricsAggregator): void { - clearInterval(aggregator['_interval']); -} - -describe('BrowserMetricsAggregator', () => { - const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); - const testClient = new TestClient(options); - - it('adds items to buckets', () => { - const aggregator = new BrowserMetricsAggregator(testClient); - aggregator.add('c', 'requests', 1); - expect(aggregator['_buckets'].size).toEqual(1); - - const firstValue = aggregator['_buckets'].values().next().value; - expect(firstValue).toEqual({ - metric: expect.any(CounterMetric), - metricType: 'c', - name: 'requests', - tags: {}, - timestamp: expect.any(Number), - unit: 'none', - }); - - _cleanupAggregator(aggregator); - }); - - it('groups same items together', () => { - const aggregator = new BrowserMetricsAggregator(testClient); - aggregator.add('c', 'requests', 1); - expect(aggregator['_buckets'].size).toEqual(1); - aggregator.add('c', 'requests', 1); - expect(aggregator['_buckets'].size).toEqual(1); - - const firstValue = aggregator['_buckets'].values().next().value; - expect(firstValue).toEqual({ - metric: expect.any(CounterMetric), - metricType: 'c', - name: 'requests', - tags: {}, - timestamp: expect.any(Number), - unit: 'none', - }); - expect(firstValue.metric._value).toEqual(2); - - _cleanupAggregator(aggregator); - }); - - it('differentiates based on tag value', () => { - const aggregator = new BrowserMetricsAggregator(testClient); - aggregator.add('g', 'cpu', 50); - expect(aggregator['_buckets'].size).toEqual(1); - aggregator.add('g', 'cpu', 55, undefined, { a: 'value' }); - expect(aggregator['_buckets'].size).toEqual(2); - - _cleanupAggregator(aggregator); - }); - - describe('serializeBuckets', () => { - it('serializes ', () => { - const aggregator = new BrowserMetricsAggregator(testClient); - aggregator.add('c', 'requests', 8); - aggregator.add('g', 'cpu', 50); - aggregator.add('g', 'cpu', 55); - aggregator.add('g', 'cpu', 52); - aggregator.add('d', 'lcp', 1, 'second', { a: 'value', b: 'anothervalue' }); - aggregator.add('d', 'lcp', 1.2, 'second', { a: 'value', b: 'anothervalue' }); - aggregator.add('s', 'important_people', 'a', 'none', { numericKey: 2 }); - aggregator.add('s', 'important_people', 'b', 'none', { numericKey: 2 }); - - const metricBuckets = Array.from(aggregator['_buckets']).map(([, bucketItem]) => bucketItem); - const serializedBuckets = serializeMetricBuckets(metricBuckets); - - expect(serializedBuckets).toContain('requests@none:8|c|T'); - expect(serializedBuckets).toContain('cpu@none:52:50:55:157:3|g|T'); - expect(serializedBuckets).toContain('lcp@second:1:1.2|d|#a:value,b:anothervalue|T'); - expect(serializedBuckets).toContain('important_people@none:97:98|s|#numericKey:2|T'); - - _cleanupAggregator(aggregator); - }); - }); -}); diff --git a/packages/core/test/lib/metrics/timing.test.ts b/packages/core/test/lib/metrics/timing.test.ts deleted file mode 100644 index 3e48047c9175..000000000000 --- a/packages/core/test/lib/metrics/timing.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* eslint-disable deprecation/deprecation */ -import { getCurrentScope, getIsolationScope, setCurrentClient } from '../../../src'; -import { MetricsAggregator } from '../../../src/metrics/aggregator'; -import { metrics as metricsCore } from '../../../src/metrics/exports'; -import { metricsDefault } from '../../../src/metrics/exports-default'; -import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; - -const PUBLIC_DSN = 'https://username@domain/123'; - -describe('metrics.timing', () => { - let testClient: TestClient; - const options = getDefaultTestClientOptions({ - dsn: PUBLIC_DSN, - tracesSampleRate: 0.0, - }); - - beforeEach(() => { - testClient = new TestClient(options); - setCurrentClient(testClient); - }); - - afterEach(() => { - getCurrentScope().setClient(undefined); - getCurrentScope().clear(); - getIsolationScope().clear(); - }); - - it('works with minimal data', async () => { - const res = metricsDefault.timing('t1', 10); - expect(res).toStrictEqual(undefined); - - const sendSpy = jest.spyOn(testClient.getTransport()!, 'send'); - - metricsCore.getMetricsAggregatorForClient(testClient, MetricsAggregator)!.flush(); - - expect(sendSpy).toHaveBeenCalledTimes(1); - expect(sendSpy).toHaveBeenCalledWith([ - { sent_at: expect.any(String) }, - [[{ length: expect.any(Number), type: 'statsd' }, expect.stringMatching(/t1@second:10\|d\|T(\d+)/)]], - ]); - }); - - it('allows to define a unit', async () => { - const res = metricsDefault.timing('t1', 10, 'hour'); - expect(res).toStrictEqual(undefined); - - const sendSpy = jest.spyOn(testClient.getTransport()!, 'send'); - - metricsCore.getMetricsAggregatorForClient(testClient, MetricsAggregator)!.flush(); - - expect(sendSpy).toHaveBeenCalledTimes(1); - expect(sendSpy).toHaveBeenCalledWith([ - { sent_at: expect.any(String) }, - [[{ length: expect.any(Number), type: 'statsd' }, expect.stringMatching(/t1@hour:10\|d\|T(\d+)/)]], - ]); - }); - - it('allows to define data', async () => { - const res = metricsDefault.timing('t1', 10, 'hour', { - tags: { tag1: 'value1', tag2: 'value2' }, - }); - expect(res).toStrictEqual(undefined); - - const sendSpy = jest.spyOn(testClient.getTransport()!, 'send'); - - metricsCore.getMetricsAggregatorForClient(testClient, MetricsAggregator)!.flush(); - - expect(sendSpy).toHaveBeenCalledTimes(1); - expect(sendSpy).toHaveBeenCalledWith([ - { sent_at: expect.any(String) }, - [ - [ - { length: expect.any(Number), type: 'statsd' }, - expect.stringMatching(/t1@hour:10\|d|#tag1:value1,tag2:value2\|T(\d+)/), - ], - ], - ]); - }); - - it('works with a sync callback', async () => { - const res = metricsDefault.timing('t1', () => { - sleepSync(200); - return 'oho'; - }); - expect(res).toStrictEqual('oho'); - - const sendSpy = jest.spyOn(testClient.getTransport()!, 'send'); - - metricsCore.getMetricsAggregatorForClient(testClient, MetricsAggregator)!.flush(); - - expect(sendSpy).toHaveBeenCalledTimes(1); - expect(sendSpy).toHaveBeenCalledWith([ - { sent_at: expect.any(String) }, - [[{ length: expect.any(Number), type: 'statsd' }, expect.stringMatching(/t1@second:(0.\d+)\|d\|T(\d+)/)]], - ]); - }); - - it('works with an async callback', async () => { - const res = metricsDefault.timing('t1', async () => { - await new Promise(resolve => setTimeout(resolve, 200)); - return 'oho'; - }); - expect(res).toBeInstanceOf(Promise); - expect(await res).toStrictEqual('oho'); - - const sendSpy = jest.spyOn(testClient.getTransport()!, 'send'); - - metricsCore.getMetricsAggregatorForClient(testClient, MetricsAggregator)!.flush(); - - expect(sendSpy).toHaveBeenCalledTimes(1); - expect(sendSpy).toHaveBeenCalledWith([ - { sent_at: expect.any(String) }, - [[{ length: expect.any(Number), type: 'statsd' }, expect.stringMatching(/t1@second:(0.\d+)\|d\|T(\d+)/)]], - ]); - }); -}); - -function sleepSync(milliseconds: number): void { - const start = Date.now(); - for (let i = 0; i < 1e7; i++) { - if (new Date().getTime() - start > milliseconds) { - break; - } - } -} diff --git a/packages/core/test/lib/metrics/utils.test.ts b/packages/core/test/lib/metrics/utils.test.ts deleted file mode 100644 index e25014715748..000000000000 --- a/packages/core/test/lib/metrics/utils.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - COUNTER_METRIC_TYPE, - DISTRIBUTION_METRIC_TYPE, - GAUGE_METRIC_TYPE, - SET_METRIC_TYPE, -} from '../../../src/metrics/constants'; -import { getBucketKey, sanitizeTags } from '../../../src/metrics/utils'; - -describe('getBucketKey', () => { - it.each([ - [COUNTER_METRIC_TYPE, 'requests', 'none', {}, 'crequestsnone'], - [GAUGE_METRIC_TYPE, 'cpu', 'none', {}, 'gcpunone'], - [DISTRIBUTION_METRIC_TYPE, 'lcp', 'second', { a: 'value', b: 'anothervalue' }, 'dlcpseconda,value,b,anothervalue'], - [DISTRIBUTION_METRIC_TYPE, 'lcp', 'second', { b: 'anothervalue', a: 'value' }, 'dlcpseconda,value,b,anothervalue'], - [DISTRIBUTION_METRIC_TYPE, 'lcp', 'second', { a: '1', b: '2', c: '3' }, 'dlcpseconda,1,b,2,c,3'], - [DISTRIBUTION_METRIC_TYPE, 'lcp', 'second', { numericKey: '2' }, 'dlcpsecondnumericKey,2'], - [SET_METRIC_TYPE, 'important_org_ids', 'none', { numericKey: '2' }, 'simportant_org_idsnonenumericKey,2'], - ])('should return', (metricType, name, unit, tags, expected) => { - expect(getBucketKey(metricType, name, unit, tags)).toEqual(expected); - }); - - it('should sanitize tags', () => { - const inputTags = { - 'f-oo|bar': '%$foo/', - 'foo$.$.$bar': 'blah{}', - 'foö-bar': 'snöwmän', - route: 'GET /foo', - __bar__: 'this | or , that', - 'foo/': 'hello!\n\r\t\\', - }; - - const outputTags = { - 'f-oobar': '%$foo/', - 'foo..bar': 'blah{}', - 'fo-bar': 'snöwmän', - route: 'GET /foo', - __bar__: 'this \\u{7c} or \\u{2c} that', - 'foo/': 'hello!\\n\\r\\t\\\\', - }; - - expect(sanitizeTags(inputTags)).toEqual(outputTags); - }); -}); diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index bd0798cc0898..9806bd06bd14 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -1,14 +1,6 @@ +import type { Client, ScopeContext } from '../../src'; import { GLOBAL_OBJ, createStackParser, getGlobalScope, getIsolationScope } from '../../src'; -import type { - Attachment, - Breadcrumb, - Client, - ClientOptions, - Event, - EventHint, - EventProcessor, - ScopeContext, -} from '../../src/types-hoist'; +import type { Attachment, Breadcrumb, ClientOptions, Event, EventHint, EventProcessor } from '../../src/types-hoist'; import { Scope } from '../../src/scope'; import { @@ -79,6 +71,45 @@ describe('applyDebugIds', () => { }), ); }); + + it('handles multiple exception values where not all events have valid stack traces', () => { + GLOBAL_OBJ._sentryDebugIds = { + 'filename1.js\nfilename1.js': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + 'filename2.js\nfilename2.js': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + }; + const stackParser = createStackParser([0, line => ({ filename: line })]); + + const event: Event = { + exception: { + values: [ + { + value: 'first exception without stack trace', + }, + { + stacktrace: { + frames: [{ filename: 'filename1.js' }, { filename: 'filename2.js' }], + }, + }, + ], + }, + }; + + applyDebugIds(event, stackParser); + + expect(event.exception?.values?.[0]).toEqual({ + value: 'first exception without stack trace', + }); + + expect(event.exception?.values?.[1]?.stacktrace?.frames).toContainEqual({ + filename: 'filename1.js', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + }); + + expect(event.exception?.values?.[1]?.stacktrace?.frames).toContainEqual({ + filename: 'filename2.js', + debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + }); + }); }); describe('applyDebugMeta', () => { @@ -121,6 +152,49 @@ describe('applyDebugMeta', () => { debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', }); }); + + it('handles multiple exception values where not all events have valid stack traces', () => { + const event: Event = { + exception: { + values: [ + { + value: 'first exception without stack trace', + }, + { + stacktrace: { + frames: [ + { filename: 'filename1.js', debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }, + { filename: 'filename2.js', debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb' }, + ], + }, + }, + ], + }, + }; + + applyDebugMeta(event); + + expect(event.exception?.values?.[0]).toEqual({ + value: 'first exception without stack trace', + }); + + expect(event.exception?.values?.[1]?.stacktrace?.frames).toEqual([ + { filename: 'filename1.js' }, + { filename: 'filename2.js' }, + ]); + + expect(event.debug_meta?.images).toContainEqual({ + type: 'sourcemap', + code_file: 'filename1.js', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + }); + + expect(event.debug_meta?.images).toContainEqual({ + type: 'sourcemap', + code_file: 'filename2.js', + debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + }); + }); }); describe('parseEventHintOrCaptureContext', () => { @@ -162,10 +236,9 @@ describe('parseEventHintOrCaptureContext', () => { contexts: { os: { name: 'linux' } }, tags: { foo: 'bar' }, fingerprint: ['xx', 'yy'], - requestSession: { status: 'ok' }, propagationContext: { traceId: 'xxx', - spanId: 'yyy', + sampleRand: Math.random(), }, }; @@ -175,9 +248,9 @@ describe('parseEventHintOrCaptureContext', () => { it('triggers a TS error if trying to mix ScopeContext & EventHint', () => { const actual = parseEventHintOrCaptureContext({ + mechanism: { handled: false }, // @ts-expect-error We are specifically testing that this errors! user: { id: 'xxx' }, - mechanism: { handled: false }, }); // ScopeContext takes presedence in this case, but this is actually not supported @@ -256,7 +329,7 @@ describe('prepareEvent', () => { tags: { tag1: 'aa', tag2: 'aa' }, extra: { extra1: 'aa', extra2: 'aa' }, contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { traceId: '1', sampleRand: 0.42 }, fingerprint: ['aa'], }); scope.addBreadcrumb(breadcrumb1); diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index 76130e779fed..026d33fd54e5 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -1,3 +1,4 @@ +import type { Client } from '../../src'; import { applyScopeDataToEvent, getCurrentScope, @@ -6,9 +7,8 @@ import { withIsolationScope, withScope, } from '../../src'; -import type { Breadcrumb, Client, Event, RequestSessionStatus } from '../../src/types-hoist'; - import { Scope } from '../../src/scope'; +import type { Breadcrumb, Event } from '../../src/types-hoist'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; import { clearGlobalScope } from './clear-global-scope'; @@ -32,7 +32,7 @@ describe('Scope', () => { eventProcessors: [], propagationContext: { traceId: expect.any(String), - spanId: expect.any(String), + sampleRand: expect.any(Number), }, sdkProcessingMetadata: {}, }); @@ -58,7 +58,7 @@ describe('Scope', () => { eventProcessors: [], propagationContext: { traceId: expect.any(String), - spanId: expect.any(String), + sampleRand: expect.any(Number), }, sdkProcessingMetadata: {}, }); @@ -92,7 +92,7 @@ describe('Scope', () => { eventProcessors: [], propagationContext: { traceId: expect.any(String), - spanId: expect.any(String), + sampleRand: expect.any(Number), }, sdkProcessingMetadata: {}, }); @@ -104,7 +104,7 @@ describe('Scope', () => { expect(scope.getScopeData().propagationContext).toEqual({ traceId: expect.any(String), - spanId: expect.any(String), + sampleRand: expect.any(Number), sampled: undefined, dsc: undefined, parentSpanId: undefined, @@ -232,14 +232,14 @@ describe('Scope', () => { const oldPropagationContext = scope.getPropagationContext(); scope.setPropagationContext({ traceId: '86f39e84263a4de99c326acab3bfe3bd', - spanId: '6e0c63257de34c92', + sampleRand: 0.42, sampled: true, }); expect(scope.getPropagationContext()).not.toEqual(oldPropagationContext); expect(scope.getPropagationContext()).toEqual({ traceId: '86f39e84263a4de99c326acab3bfe3bd', - spanId: '6e0c63257de34c92', sampled: true, + sampleRand: 0.42, }); }); @@ -259,15 +259,6 @@ describe('Scope', () => { expect(parentScope['_extra']).toEqual(scope['_extra']); }); - test('_requestSession clone', () => { - const parentScope = new Scope(); - // eslint-disable-next-line deprecation/deprecation - parentScope.setRequestSession({ status: 'errored' }); - const scope = parentScope.clone(); - // eslint-disable-next-line deprecation/deprecation - expect(parentScope.getRequestSession()).toEqual(scope.getRequestSession()); - }); - test('parent changed inheritance', () => { const parentScope = new Scope(); const scope = parentScope.clone(); @@ -286,26 +277,6 @@ describe('Scope', () => { expect(scope['_extra']).toEqual({ a: 2 }); }); - test('child override should set the value of parent _requestSession', () => { - // Test that ensures if the status value of `status` of `_requestSession` is changed in a child scope - // that it should also change in parent scope because we are copying the reference to the object - const parentScope = new Scope(); - // eslint-disable-next-line deprecation/deprecation - parentScope.setRequestSession({ status: 'errored' }); - - const scope = parentScope.clone(); - // eslint-disable-next-line deprecation/deprecation - const requestSession = scope.getRequestSession(); - if (requestSession) { - requestSession.status = 'ok'; - } - - // eslint-disable-next-line deprecation/deprecation - expect(parentScope.getRequestSession()).toEqual({ status: 'ok' }); - // eslint-disable-next-line deprecation/deprecation - expect(scope.getRequestSession()).toEqual({ status: 'ok' }); - }); - test('should clone propagation context', () => { const parentScope = new Scope(); const scope = parentScope.clone(); @@ -322,16 +293,13 @@ describe('Scope', () => { scope.setUser({ id: '1' }); scope.setFingerprint(['abcd']); scope.addBreadcrumb({ message: 'test' }); - // eslint-disable-next-line deprecation/deprecation - scope.setRequestSession({ status: 'ok' }); expect(scope['_extra']).toEqual({ a: 2 }); scope.clear(); expect(scope['_extra']).toEqual({}); - expect(scope['_requestSession']).toEqual(undefined); expect(scope['_propagationContext']).toEqual({ traceId: expect.any(String), - spanId: expect.any(String), sampled: undefined, + sampleRand: expect.any(Number), }); expect(scope['_propagationContext']).not.toEqual(oldPropagationContext); }); @@ -356,8 +324,6 @@ describe('Scope', () => { scope.setUser({ id: '1337' }); scope.setLevel('info'); scope.setFingerprint(['foo']); - // eslint-disable-next-line deprecation/deprecation - scope.setRequestSession({ status: 'ok' }); }); test('given no data, returns the original scope', () => { @@ -405,7 +371,6 @@ describe('Scope', () => { localScope.setUser({ id: '42' }); localScope.setLevel('warning'); localScope.setFingerprint(['bar']); - (localScope as any)._requestSession = { status: 'ok' }; const updatedScope = scope.update(localScope) as any; @@ -427,7 +392,6 @@ describe('Scope', () => { expect(updatedScope._user).toEqual({ id: '42' }); expect(updatedScope._level).toEqual('warning'); expect(updatedScope._fingerprint).toEqual(['bar']); - expect(updatedScope._requestSession.status).toEqual('ok'); // @ts-expect-error accessing private property for test expect(updatedScope._propagationContext).toEqual(localScope._propagationContext); }); @@ -450,7 +414,6 @@ describe('Scope', () => { expect(updatedScope._user).toEqual({ id: '1337' }); expect(updatedScope._level).toEqual('info'); expect(updatedScope._fingerprint).toEqual(['foo']); - expect(updatedScope._requestSession.status).toEqual('ok'); }); test('given a plain object, it should merge two together, with the passed object having priority', () => { @@ -461,12 +424,10 @@ describe('Scope', () => { level: 'warning' as const, tags: { bar: '3', baz: '4' }, user: { id: '42' }, - // eslint-disable-next-line deprecation/deprecation - requestSession: { status: 'errored' as RequestSessionStatus }, propagationContext: { traceId: '8949daf83f4a4a70bee4c1eb9ab242ed', - spanId: 'a024ad8fea82680e', sampled: true, + sampleRand: 0.42, }, }; @@ -490,11 +451,10 @@ describe('Scope', () => { expect(updatedScope._user).toEqual({ id: '42' }); expect(updatedScope._level).toEqual('warning'); expect(updatedScope._fingerprint).toEqual(['bar']); - expect(updatedScope._requestSession).toEqual({ status: 'errored' }); expect(updatedScope._propagationContext).toEqual({ traceId: '8949daf83f4a4a70bee4c1eb9ab242ed', - spanId: 'a024ad8fea82680e', sampled: true, + sampleRand: 0.42, }); }); }); @@ -542,7 +502,7 @@ describe('Scope', () => { tags: { tag1: 'aa', tag2: 'aa' }, extra: { extra1: 'aa', extra2: 'aa' }, contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { traceId: '1', sampleRand: 0.42 }, fingerprint: ['aa'], }); scope.addBreadcrumb(breadcrumb1); diff --git a/packages/core/test/lib/sdk.test.ts b/packages/core/test/lib/sdk.test.ts index 9da1dec65789..3cce0b5a9020 100644 --- a/packages/core/test/lib/sdk.test.ts +++ b/packages/core/test/lib/sdk.test.ts @@ -1,8 +1,8 @@ +import type { Client } from '../../src'; import { captureCheckIn, getCurrentScope, setCurrentClient } from '../../src'; -import type { Client, Integration } from '../../src/types-hoist'; - import { installedIntegrations } from '../../src/integration'; import { initAndBind } from '../../src/sdk'; +import type { Integration } from '../../src/types-hoist'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; // eslint-disable-next-line no-var diff --git a/packages/core/test/lib/serverruntimeclient.test.ts b/packages/core/test/lib/server-runtime-client.test.ts similarity index 93% rename from packages/core/test/lib/serverruntimeclient.test.ts rename to packages/core/test/lib/server-runtime-client.test.ts index bdf1c5242b80..2eeb90083f29 100644 --- a/packages/core/test/lib/serverruntimeclient.test.ts +++ b/packages/core/test/lib/server-runtime-client.test.ts @@ -1,6 +1,6 @@ import type { Event, EventHint } from '../../src/types-hoist'; -import { createTransport } from '../../src'; +import { Scope, createTransport } from '../../src'; import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client'; import { ServerRuntimeClient } from '../../src/server-runtime-client'; @@ -18,6 +18,9 @@ function getDefaultClientOptions(options: Partial = describe('ServerRuntimeClient', () => { let client: ServerRuntimeClient; + const currentScope = new Scope(); + const isolationScope = new Scope(); + describe('_prepareEvent', () => { test('adds platform to event', () => { const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); @@ -25,7 +28,7 @@ describe('ServerRuntimeClient', () => { const event: Event = {}; const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); + client['_prepareEvent'](event, hint, currentScope, isolationScope); expect(event.platform).toEqual('blargh'); }); @@ -36,7 +39,7 @@ describe('ServerRuntimeClient', () => { const event: Event = {}; const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); + client['_prepareEvent'](event, hint, currentScope, isolationScope); expect(event.server_name).toEqual('server'); }); @@ -47,7 +50,7 @@ describe('ServerRuntimeClient', () => { const event: Event = {}; const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); + client['_prepareEvent'](event, hint, currentScope, isolationScope); expect(event.contexts?.runtime).toEqual({ name: 'edge', @@ -60,7 +63,7 @@ describe('ServerRuntimeClient', () => { const event: Event = { contexts: { runtime: { name: 'foo', version: '1.2.3' } } }; const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); + client['_prepareEvent'](event, hint, currentScope, isolationScope); expect(event.contexts?.runtime).toEqual({ name: 'foo', version: '1.2.3' }); expect(event.contexts?.runtime).not.toEqual({ name: 'edge' }); diff --git a/packages/core/test/lib/sessionflusher.test.ts b/packages/core/test/lib/sessionflusher.test.ts deleted file mode 100644 index 53fe930b58c2..000000000000 --- a/packages/core/test/lib/sessionflusher.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { Client } from '../../src/types-hoist'; - -import { SessionFlusher } from '../../src/sessionflusher'; - -describe('Session Flusher', () => { - let sendSession: jest.Mock; - let mockClient: Client; - - beforeEach(() => { - jest.useFakeTimers(); - sendSession = jest.fn(() => Promise.resolve({ status: 'success' })); - mockClient = { - sendSession, - } as unknown as Client; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - test('test incrementSessionStatusCount updates the internal SessionFlusher state', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); - - const date = new Date('2021-04-08T12:18:23.043Z'); - let count = (flusher as any)._incrementSessionStatusCount('ok', date); - expect(count).toEqual(1); - count = (flusher as any)._incrementSessionStatusCount('ok', date); - expect(count).toEqual(2); - count = (flusher as any)._incrementSessionStatusCount('errored', date); - expect(count).toEqual(1); - date.setMinutes(date.getMinutes() + 1); - count = (flusher as any)._incrementSessionStatusCount('ok', date); - expect(count).toEqual(1); - count = (flusher as any)._incrementSessionStatusCount('errored', date); - expect(count).toEqual(1); - - expect(flusher.getSessionAggregates().aggregates).toEqual([ - { errored: 1, exited: 2, started: '2021-04-08T12:18:00.000Z' }, - { errored: 1, exited: 1, started: '2021-04-08T12:19:00.000Z' }, - ]); - expect(flusher.getSessionAggregates().attrs).toEqual({ release: '1.0.0', environment: 'dev' }); - }); - - test('test undefined attributes are excluded, on incrementSessionStatusCount call', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.0' }); - - const date = new Date('2021-04-08T12:18:23.043Z'); - (flusher as any)._incrementSessionStatusCount('ok', date); - (flusher as any)._incrementSessionStatusCount('errored', date); - - expect(flusher.getSessionAggregates()).toEqual({ - aggregates: [{ errored: 1, exited: 1, started: '2021-04-08T12:18:00.000Z' }], - attrs: { release: '1.0.0' }, - }); - }); - - test('flush is called every 60 seconds after initialisation of an instance of SessionFlusher', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); - jest.advanceTimersByTime(59000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(0); - jest.advanceTimersByTime(2000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(58000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(2000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(2); - }); - - test('sendSessions is called on flush if sessions were captured', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); - const date = new Date('2021-04-08T12:18:23.043Z'); - (flusher as any)._incrementSessionStatusCount('ok', date); - (flusher as any)._incrementSessionStatusCount('ok', date); - - expect(sendSession).toHaveBeenCalledTimes(0); - - jest.advanceTimersByTime(61000); - - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - expect(sendSession).toHaveBeenCalledWith( - expect.objectContaining({ - attrs: { release: '1.0.0', environment: 'dev' }, - aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }], - }), - ); - }); - - test('sendSessions is not called on flush if no sessions were captured', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); - - expect(sendSession).toHaveBeenCalledTimes(0); - jest.advanceTimersByTime(61000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - expect(sendSession).toHaveBeenCalledTimes(0); - }); - - test('calling close on SessionFlusher should disable SessionFlusher', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.x' }); - flusher.close(); - expect((flusher as any)._isEnabled).toEqual(false); - }); - - test('calling close on SessionFlusher will force call flush', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.x' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); - const date = new Date('2021-04-08T12:18:23.043Z'); - (flusher as any)._incrementSessionStatusCount('ok', date); - (flusher as any)._incrementSessionStatusCount('ok', date); - flusher.close(); - - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - expect(sendSession).toHaveBeenCalledWith( - expect.objectContaining({ - attrs: { release: '1.0.x' }, - aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }], - }), - ); - }); -}); diff --git a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts index 579f90fbad56..63a6910de06b 100644 --- a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts +++ b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts @@ -42,8 +42,12 @@ describe('getDynamicSamplingContextFromSpan', () => { spanId: '12345', traceFlags: 0, traceState: { - get() { - return 'sentry-environment=myEnv2'; + get(key: string) { + if (key === 'sentry.dsc') { + return 'sentry-environment=myEnv2'; + } else { + return undefined; + } }, } as unknown as SpanContextData['traceState'], }; @@ -71,6 +75,7 @@ describe('getDynamicSamplingContextFromSpan', () => { sample_rate: '0.56', trace_id: expect.stringMatching(/^[a-f0-9]{32}$/), transaction: 'tx', + sample_rand: expect.any(String), }); }); @@ -88,6 +93,7 @@ describe('getDynamicSamplingContextFromSpan', () => { sample_rate: '1', trace_id: expect.stringMatching(/^[a-f0-9]{32}$/), transaction: 'tx', + sample_rand: expect.any(String), }); }); @@ -110,6 +116,7 @@ describe('getDynamicSamplingContextFromSpan', () => { sample_rate: '0.56', trace_id: expect.stringMatching(/^[a-f0-9]{32}$/), transaction: 'tx', + sample_rand: undefined, // this is a bit funky admittedly }); }); diff --git a/packages/core/test/lib/tracing/errors.test.ts b/packages/core/test/lib/tracing/errors.test.ts index 7b3f08a6765b..43c6e8eca0ec 100644 --- a/packages/core/test/lib/tracing/errors.test.ts +++ b/packages/core/test/lib/tracing/errors.test.ts @@ -24,7 +24,7 @@ describe('registerErrorHandlers()', () => { beforeEach(() => { mockAddGlobalErrorInstrumentationHandler.mockClear(); mockAddGlobalUnhandledRejectionInstrumentationHandler.mockClear(); - const options = getDefaultTestClientOptions({ enableTracing: true }); + const options = getDefaultTestClientOptions({ tracesSampleRate: 1 }); const client = new TestClient(options); setCurrentClient(client); client.init(); diff --git a/packages/core/test/lib/tracing/idleSpan.test.ts b/packages/core/test/lib/tracing/idleSpan.test.ts index 3d720f383173..5280be561067 100644 --- a/packages/core/test/lib/tracing/idleSpan.test.ts +++ b/packages/core/test/lib/tracing/idleSpan.test.ts @@ -7,6 +7,7 @@ import { getActiveSpan, getClient, getCurrentScope, + getDynamicSamplingContextFromSpan, getGlobalScope, getIsolationScope, setCurrentClient, @@ -60,6 +61,14 @@ describe('startIdleSpan', () => { const idleSpan = startIdleSpan({ name: 'foo' }); expect(idleSpan).toBeDefined(); expect(idleSpan).toBeInstanceOf(SentryNonRecordingSpan); + // DSC is still correctly set on the span + expect(getDynamicSamplingContextFromSpan(idleSpan)).toEqual({ + environment: 'production', + public_key: '123', + sample_rate: '0', + sampled: 'false', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }); // not set as active span, though expect(getActiveSpan()).toBe(undefined); diff --git a/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts b/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts index 373e81946093..71bb78738f4f 100644 --- a/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts +++ b/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts @@ -18,6 +18,8 @@ describe('SentryNonRecordingSpan', () => { expect(spanToJSON(span)).toEqual({ span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: {}, + start_timestamp: 0, }); // Ensure all methods work @@ -32,6 +34,8 @@ describe('SentryNonRecordingSpan', () => { expect(spanToJSON(span)).toEqual({ span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: {}, + start_timestamp: 0, }); }); }); diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index 9b2cad83f2e1..cfc3745f4387 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -1,3 +1,4 @@ +import type { SpanJSON } from '../../../src'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getCurrentScope, setCurrentClient, timestampInSeconds } from '../../../src'; import { SentrySpan } from '../../../src/tracing/sentrySpan'; import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus'; @@ -30,7 +31,7 @@ describe('SentrySpan', () => { const spanJson = spanToJSON(span); expect(spanJson.description).toEqual('new name'); - expect(spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('custom'); + expect(spanJson.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('custom'); }); }); @@ -176,10 +177,10 @@ describe('SentrySpan', () => { expect(mockSend).toHaveBeenCalled(); }); - test('does not send the span if `beforeSendSpan` drops the span', () => { + test('does not drop the span if `beforeSendSpan` returns null', () => { const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); - const beforeSendSpan = jest.fn(() => null); + const beforeSendSpan = jest.fn(() => null as unknown as SpanJSON); const client = new TestClient( getDefaultTestClientOptions({ dsn: 'https://username@domain/123', @@ -201,12 +202,11 @@ describe('SentrySpan', () => { }); span.end(); - expect(mockSend).not.toHaveBeenCalled(); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'span'); + expect(mockSend).toHaveBeenCalled(); + expect(recordDroppedEventSpy).not.toHaveBeenCalled(); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toBeCalledWith( - '[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.', + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[Sentry] Returning null from `beforeSendSpan` is disallowed. To drop certain spans, configure the respective integrations directly.', ); consoleWarnSpy.mockRestore(); }); @@ -231,7 +231,6 @@ describe('SentrySpan', () => { expect(captureEventSpy).toHaveBeenCalledTimes(1); expect(captureEventSpy).toHaveBeenCalledWith({ - _metrics_summary: undefined, contexts: { trace: { data: { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index ac7605481649..0eee7338a93d 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -15,6 +15,7 @@ import { getAsyncContextStrategy } from '../../../src/asyncContext'; import { SentrySpan, continueTrace, + getDynamicSamplingContextFromSpan, registerSpanErrorInstrumentation, startInactiveSpan, startSpan, @@ -152,9 +153,7 @@ describe('startSpan', () => { try { await startSpan({ name: 'GET users/[id]' }, () => { return startSpan({ name: 'SELECT * from users' }, childSpan => { - if (childSpan) { - childSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query'); - } + childSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query'); return callback(); }); }); @@ -219,6 +218,13 @@ describe('startSpan', () => { expect(span).toBeDefined(); expect(span).toBeInstanceOf(SentryNonRecordingSpan); + expect(getDynamicSamplingContextFromSpan(span)).toEqual({ + environment: 'production', + sample_rate: '0', + sampled: 'false', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + transaction: 'GET users/[id]', + }); }); it('creates & finishes span', async () => { @@ -253,24 +259,127 @@ describe('startSpan', () => { expect(getActiveSpan()).toBe(undefined); }); - it('allows to pass a scope', () => { + it('starts the span on the fork of a passed custom scope', () => { const initialScope = getCurrentScope(); - const manualScope = initialScope.clone(); + const customScope = initialScope.clone(); + customScope.setTag('dogs', 'great'); + const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true }); - _setSpanForScope(manualScope, parentSpan); + _setSpanForScope(customScope, parentSpan); - startSpan({ name: 'GET users/[id]', scope: manualScope }, span => { + startSpan({ name: 'GET users/[id]', scope: customScope }, span => { + // current scope is forked from the customScope expect(getCurrentScope()).not.toBe(initialScope); - expect(getCurrentScope()).toBe(manualScope); + expect(getCurrentScope()).not.toBe(customScope); + expect(getCurrentScope().getScopeData().tags).toEqual({ dogs: 'great' }); + + // active span is set correctly expect(getActiveSpan()).toBe(span); + + // span has the correct parent span expect(spanToJSON(span).parent_span_id).toBe('parent-span-id'); + + // scope data modifications + getCurrentScope().setTag('cats', 'great'); + customScope.setTag('bears', 'great'); + + expect(getCurrentScope().getScopeData().tags).toEqual({ dogs: 'great', cats: 'great' }); + expect(customScope.getScopeData().tags).toEqual({ dogs: 'great', bears: 'great' }); + }); + + // customScope modifications are persisted + expect(customScope.getScopeData().tags).toEqual({ dogs: 'great', bears: 'great' }); + + // span is parent span again on customScope + withScope(customScope, () => { + expect(getActiveSpan()).toBe(parentSpan); }); + // but activeSpan and currentScope are reset, since customScope was never active expect(getCurrentScope()).toBe(initialScope); expect(getActiveSpan()).toBe(undefined); }); + describe('handles multiple spans in sequence with a custom scope', () => { + it('with parent span', () => { + const initialScope = getCurrentScope(); + + const customScope = initialScope.clone(); + const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true }); + _setSpanForScope(customScope, parentSpan); + + startSpan({ name: 'span 1', scope: customScope }, span1 => { + // current scope is forked from the customScope + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).not.toBe(customScope); + + expect(getActiveSpan()).toBe(span1); + expect(spanToJSON(span1).parent_span_id).toBe('parent-span-id'); + }); + + // active span on customScope is reset + withScope(customScope, () => { + expect(getActiveSpan()).toBe(parentSpan); + }); + + startSpan({ name: 'span 2', scope: customScope }, span2 => { + // current scope is forked from the customScope + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).not.toBe(customScope); + + expect(getActiveSpan()).toBe(span2); + // both, span1 and span2 are children of the parent span + expect(spanToJSON(span2).parent_span_id).toBe('parent-span-id'); + }); + + withScope(customScope, () => { + expect(getActiveSpan()).toBe(parentSpan); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); + }); + + it('without parent span', () => { + const initialScope = getCurrentScope(); + const customScope = initialScope.clone(); + + const traceId = customScope.getPropagationContext()?.traceId; + + startSpan({ name: 'span 1', scope: customScope }, span1 => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).not.toBe(customScope); + + expect(getActiveSpan()).toBe(span1); + expect(getRootSpan(getActiveSpan()!)).toBe(span1); + + expect(span1.spanContext().traceId).toBe(traceId); + }); + + withScope(customScope, () => { + expect(getActiveSpan()).toBe(undefined); + }); + + startSpan({ name: 'span 2', scope: customScope }, span2 => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).not.toBe(customScope); + + expect(getActiveSpan()).toBe(span2); + expect(getRootSpan(getActiveSpan()!)).toBe(span2); + + expect(span2.spanContext().traceId).toBe(traceId); + }); + + withScope(customScope, () => { + expect(getActiveSpan()).toBe(undefined); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); + }); + }); + it('allows to pass a parentSpan', () => { const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true, name: 'parent-span' }); @@ -364,6 +473,7 @@ describe('startSpan', () => { sample_rate: '1', transaction: 'outer transaction', sampled: 'true', + sample_rand: expect.any(String), }, }); @@ -371,7 +481,6 @@ describe('startSpan', () => { trace: { data: { 'sentry.source': 'custom', - 'sentry.sample_rate': 1, 'sentry.origin': 'manual', }, parent_span_id: innerParentSpanId, @@ -389,6 +498,7 @@ describe('startSpan', () => { sample_rate: '1', transaction: 'outer transaction', sampled: 'true', + sample_rand: expect.any(String), }, }); }); @@ -398,7 +508,7 @@ describe('startSpan', () => { withScope(scope => { scope.setPropagationContext({ traceId: '99999999999999999999999999999999', - spanId: '1212121212121212', + sampleRand: Math.random(), dsc: {}, parentSpanId: '4242424242424242', }); @@ -498,7 +608,6 @@ describe('startSpan', () => { test2: 'aa', test3: 'bb', }, - transactionContext: expect.objectContaining({ name: 'outer', parentSampled: undefined }), }); }); @@ -636,6 +745,13 @@ describe('startSpanManual', () => { expect(span).toBeDefined(); expect(span).toBeInstanceOf(SentryNonRecordingSpan); + expect(getDynamicSamplingContextFromSpan(span)).toEqual({ + environment: 'production', + sample_rate: '0', + sampled: 'false', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + transaction: 'GET users/[id]', + }); }); it('creates & finishes span', async () => { @@ -665,27 +781,100 @@ describe('startSpanManual', () => { expect(getActiveSpan()).toBe(undefined); }); - it('allows to pass a scope', () => { - const initialScope = getCurrentScope(); + describe('starts a span on the fork of a custom scope if passed', () => { + it('with parent span', () => { + const initialScope = getCurrentScope(); - const manualScope = initialScope.clone(); - const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true }); - _setSpanForScope(manualScope, parentSpan); + const customScope = initialScope.clone(); + customScope.setTag('dogs', 'great'); - startSpanManual({ name: 'GET users/[id]', scope: manualScope }, span => { - expect(getCurrentScope()).not.toBe(initialScope); - expect(getCurrentScope()).toBe(manualScope); - expect(getActiveSpan()).toBe(span); - expect(spanToJSON(span).parent_span_id).toBe('parent-span-id'); + const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true }); + _setSpanForScope(customScope, parentSpan); - span.end(); + startSpanManual({ name: 'GET users/[id]', scope: customScope }, span => { + // current scope is forked from the customScope + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).not.toBe(customScope); + expect(spanToJSON(span).parent_span_id).toBe('parent-span-id'); - // Is still the active span - expect(getActiveSpan()).toBe(span); + // span is active span + expect(getActiveSpan()).toBe(span); + + span.end(); + + // span is still the active span (weird but it is what it is) + expect(getActiveSpan()).toBe(span); + + getCurrentScope().setTag('cats', 'great'); + customScope.setTag('bears', 'great'); + + expect(getCurrentScope().getScopeData().tags).toEqual({ dogs: 'great', cats: 'great' }); + expect(customScope.getScopeData().tags).toEqual({ dogs: 'great', bears: 'great' }); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); + + startSpanManual({ name: 'POST users/[id]', scope: customScope }, (span, finish) => { + // current scope is forked from the customScope + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).not.toBe(customScope); + expect(spanToJSON(span).parent_span_id).toBe('parent-span-id'); + + // scope data modification from customScope in previous callback is persisted + expect(getCurrentScope().getScopeData().tags).toEqual({ dogs: 'great', bears: 'great' }); + + // span is active span + expect(getActiveSpan()).toBe(span); + + // calling finish() or span.end() has the same effect + finish(); + + // using finish() resets the scope correctly + expect(getActiveSpan()).toBe(span); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); }); - expect(getCurrentScope()).toBe(initialScope); - expect(getActiveSpan()).toBe(undefined); + it('without parent span', () => { + const initialScope = getCurrentScope(); + const manualScope = initialScope.clone(); + + startSpanManual({ name: 'GET users/[id]', scope: manualScope }, span => { + // current scope is forked from the customScope + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).not.toBe(manualScope); + expect(getCurrentScope()).toEqual(manualScope); + + // span is active span and a root span + expect(getActiveSpan()).toBe(span); + expect(getRootSpan(span)).toBe(span); + + span.end(); + + expect(getActiveSpan()).toBe(span); + }); + + startSpanManual({ name: 'POST users/[id]', scope: manualScope }, (span, finish) => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).not.toBe(manualScope); + expect(getCurrentScope()).toEqual(manualScope); + + // second span is active span and its own root span + expect(getActiveSpan()).toBe(span); + expect(getRootSpan(span)).toBe(span); + + finish(); + + // calling finish() or span.end() has the same effect + expect(getActiveSpan()).toBe(span); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); + }); }); it('allows to pass a parentSpan', () => { @@ -788,6 +977,7 @@ describe('startSpanManual', () => { sample_rate: '1', transaction: 'outer transaction', sampled: 'true', + sample_rand: expect.any(String), }, }); @@ -795,7 +985,6 @@ describe('startSpanManual', () => { trace: { data: { 'sentry.source': 'custom', - 'sentry.sample_rate': 1, 'sentry.origin': 'manual', }, parent_span_id: innerParentSpanId, @@ -813,6 +1002,7 @@ describe('startSpanManual', () => { sample_rate: '1', transaction: 'outer transaction', sampled: 'true', + sample_rand: expect.any(String), }, }); }); @@ -831,7 +1021,7 @@ describe('startSpanManual', () => { withScope(scope => { scope.setPropagationContext({ traceId: '99999999999999999999999999999991', - spanId: '1212121212121212', + sampleRand: Math.random(), dsc: {}, parentSpanId: '4242424242424242', }); @@ -975,6 +1165,13 @@ describe('startInactiveSpan', () => { expect(span).toBeDefined(); expect(span).toBeInstanceOf(SentryNonRecordingSpan); + expect(getDynamicSamplingContextFromSpan(span)).toEqual({ + environment: 'production', + sample_rate: '0', + sampled: 'false', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + transaction: 'GET users/[id]', + }); }); it('creates & finishes span', async () => { @@ -1110,6 +1307,7 @@ describe('startInactiveSpan', () => { sample_rate: '1', transaction: 'outer transaction', sampled: 'true', + sample_rand: expect.any(String), }, }); @@ -1117,7 +1315,6 @@ describe('startInactiveSpan', () => { trace: { data: { 'sentry.source': 'custom', - 'sentry.sample_rate': 1, 'sentry.origin': 'manual', }, parent_span_id: innerParentSpanId, @@ -1135,6 +1332,7 @@ describe('startInactiveSpan', () => { sample_rate: '1', transaction: 'outer transaction', sampled: 'true', + sample_rand: expect.any(String), }, }); }); @@ -1149,7 +1347,7 @@ describe('startInactiveSpan', () => { withScope(scope => { scope.setPropagationContext({ traceId: '99999999999999999999999999999991', - spanId: '1212121212121212', + sampleRand: Math.random(), dsc: {}, parentSpanId: '4242424242424242', }); @@ -1331,8 +1529,8 @@ describe('continueTrace', () => { expect(scope.getPropagationContext()).toEqual({ sampled: undefined, - spanId: expect.any(String), traceId: expect.any(String), + sampleRand: expect.any(Number), }); expect(scope.getScopeData().sdkProcessingMetadata).toEqual({}); @@ -1353,8 +1551,8 @@ describe('continueTrace', () => { dsc: {}, // DSC should be an empty object (frozen), because there was an incoming trace sampled: false, parentSpanId: '1121201211212012', - spanId: expect.any(String), traceId: '12312012123120121231201212312012', + sampleRand: expect.any(Number), }); expect(scope.getScopeData().sdkProcessingMetadata).toEqual({}); @@ -1375,11 +1573,12 @@ describe('continueTrace', () => { dsc: { environment: 'production', version: '1.0', + sample_rand: expect.any(String), }, sampled: true, parentSpanId: '1121201211212012', - spanId: expect.any(String), traceId: '12312012123120121231201212312012', + sampleRand: expect.any(Number), }); expect(scope.getScopeData().sdkProcessingMetadata).toEqual({}); @@ -1400,11 +1599,12 @@ describe('continueTrace', () => { dsc: { environment: 'production', version: '1.0', + sample_rand: expect.any(String), }, sampled: true, parentSpanId: '1121201211212012', - spanId: expect.any(String), traceId: '12312012123120121231201212312012', + sampleRand: expect.any(Number), }); expect(scope.getScopeData().sdkProcessingMetadata).toEqual({}); @@ -1481,7 +1681,7 @@ describe('withActiveSpan()', () => { setAsyncContextStrategy(undefined); - const options = getDefaultTestClientOptions({ enableTracing: true }); + const options = getDefaultTestClientOptions({ tracesSampleRate: 1 }); const client = new TestClient(options); setCurrentClient(client); client.init(); diff --git a/packages/core/test/lib/transports/base.test.ts b/packages/core/test/lib/transports/base.test.ts index 3c21c8bd70ed..70179f535efe 100644 --- a/packages/core/test/lib/transports/base.test.ts +++ b/packages/core/test/lib/transports/base.test.ts @@ -135,9 +135,7 @@ describe('createTransport', () => { await transport.send(ERROR_ENVELOPE); expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error', { - event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', - }); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); @@ -179,9 +177,7 @@ describe('createTransport', () => { await transport.send(ERROR_ENVELOPE); // Error envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error', { - event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', - }); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); @@ -223,23 +219,19 @@ describe('createTransport', () => { await transport.send(TRANSACTION_ENVELOPE); // Transaction envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'transaction', { - event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', - }); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'transaction'); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); await transport.send(ERROR_ENVELOPE); // Error envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error', { - event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', - }); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); await transport.send(ATTACHMENT_ENVELOPE); // Attachment envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'attachment', undefined); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'attachment'); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); @@ -287,17 +279,13 @@ describe('createTransport', () => { await transport.send(TRANSACTION_ENVELOPE); // Transaction envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'transaction', { - event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', - }); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'transaction'); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); await transport.send(ERROR_ENVELOPE); // Error envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error', { - event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', - }); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); diff --git a/packages/core/test/lib/transports/multiplexed.test.ts b/packages/core/test/lib/transports/multiplexed.test.ts index 647acbdc856e..024ea7e4bd8a 100644 --- a/packages/core/test/lib/transports/multiplexed.test.ts +++ b/packages/core/test/lib/transports/multiplexed.test.ts @@ -118,7 +118,7 @@ describe('makeMultiplexedTransport', () => { const makeTransport = makeMultiplexedTransport( createTestTransport((url, _, env) => { expect(url).toBe(DSN2_URL); - expect(env[0]?.dsn).toBe(DSN2); + expect(env[0].dsn).toBe(DSN2); }), () => [DSN2], ); @@ -134,7 +134,7 @@ describe('makeMultiplexedTransport', () => { createTestTransport((url, release, env) => { expect(url).toBe(DSN2_URL); expect(release).toBe('something@1.0.0'); - expect(env[0]?.dsn).toBe(DSN2); + expect(env[0].dsn).toBe(DSN2); }), () => [{ dsn: DSN2, release: 'something@1.0.0' }], ); @@ -150,7 +150,7 @@ describe('makeMultiplexedTransport', () => { createTestTransport((url, release, env) => { expect(url).toBe('http://google.com'); expect(release).toBe('something@1.0.0'); - expect(env[0]?.dsn).toBe(DSN2); + expect(env[0].dsn).toBe(DSN2); }), () => [{ dsn: DSN2, release: 'something@1.0.0' }], ); diff --git a/packages/core/test/lib/transports/offline.test.ts b/packages/core/test/lib/transports/offline.test.ts index 5aa29596fa25..2f4df73ddae9 100644 --- a/packages/core/test/lib/transports/offline.test.ts +++ b/packages/core/test/lib/transports/offline.test.ts @@ -328,7 +328,7 @@ describe('makeOfflineTransport', () => { // When it gets shifted out of the store, the sent_at header should be updated const envelopes = getSentEnvelopes().map(parseEnvelope) as EventEnvelope[]; expect(envelopes[0]?.[0]).toBeDefined(); - const sent_at = new Date(envelopes[0]![0]?.sent_at); + const sent_at = new Date(envelopes[0]![0].sent_at); expect(sent_at.getTime()).toBeGreaterThan(testStartTime.getTime()); }, diff --git a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts index cb2757c95301..077190670aba 100644 --- a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts +++ b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts @@ -1,5 +1,6 @@ +import type { ScopeData } from '../../../src'; import { startInactiveSpan } from '../../../src'; -import type { Attachment, Breadcrumb, Event, EventProcessor, EventType, ScopeData } from '../../../src/types-hoist'; +import type { Attachment, Breadcrumb, Event, EventProcessor, EventType } from '../../../src/types-hoist'; import { applyScopeDataToEvent, mergeAndOverwriteScopeData, @@ -81,7 +82,7 @@ describe('mergeScopeData', () => { extra: {}, contexts: {}, attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { traceId: '1', sampleRand: 0.42 }, sdkProcessingMetadata: {}, fingerprint: [], }; @@ -93,7 +94,7 @@ describe('mergeScopeData', () => { extra: {}, contexts: {}, attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { traceId: '1', sampleRand: 0.42 }, sdkProcessingMetadata: {}, fingerprint: [], }; @@ -106,7 +107,7 @@ describe('mergeScopeData', () => { extra: {}, contexts: {}, attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { traceId: '1', sampleRand: 0.42 }, sdkProcessingMetadata: {}, fingerprint: [], }); @@ -133,7 +134,7 @@ describe('mergeScopeData', () => { extra: { extra1: 'aa', extra2: 'aa' }, contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, attachments: [attachment1], - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { traceId: '1', sampleRand: 0.42 }, sdkProcessingMetadata: { aa: 'aa', bb: 'aa', @@ -153,7 +154,7 @@ describe('mergeScopeData', () => { extra: { extra2: 'bb', extra3: 'bb' }, contexts: { os: { name: 'os2' } }, attachments: [attachment2, attachment3], - propagationContext: { spanId: '2', traceId: '2' }, + propagationContext: { traceId: '2', sampleRand: 0.42 }, sdkProcessingMetadata: { bb: 'bb', cc: 'bb', @@ -174,7 +175,7 @@ describe('mergeScopeData', () => { extra: { extra1: 'aa', extra2: 'bb', extra3: 'bb' }, contexts: { os: { name: 'os2' }, culture: { display_name: 'name1' } }, attachments: [attachment1, attachment2, attachment3], - propagationContext: { spanId: '2', traceId: '2' }, + propagationContext: { traceId: '2', sampleRand: 0.42 }, sdkProcessingMetadata: { aa: 'aa', bb: 'bb', @@ -201,7 +202,7 @@ describe('applyScopeDataToEvent', () => { extra: {}, contexts: {}, attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { traceId: '1', sampleRand: 0.42 }, sdkProcessingMetadata: {}, fingerprint: [], transactionName: 'foo', @@ -222,7 +223,7 @@ describe('applyScopeDataToEvent', () => { extra: {}, contexts: {}, attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { traceId: '1', sampleRand: 0.42 }, sdkProcessingMetadata: {}, fingerprint: [], transactionName: 'foo', @@ -253,7 +254,7 @@ describe('applyScopeDataToEvent', () => { extra: {}, contexts: {}, attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { traceId: '1', sampleRand: 0.42 }, sdkProcessingMetadata: {}, fingerprint: [], transactionName: '/users/:id', @@ -277,7 +278,7 @@ describe('applyScopeDataToEvent', () => { extra: {}, contexts: {}, attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { traceId: '1', sampleRand: 0.42 }, sdkProcessingMetadata: {}, fingerprint: [], transactionName: 'foo', diff --git a/packages/core/test/utils-hoist/cookie.test.ts b/packages/core/test/lib/utils/cookie.test.ts similarity index 97% rename from packages/core/test/utils-hoist/cookie.test.ts rename to packages/core/test/lib/utils/cookie.test.ts index eca98a592f00..b41d2a4fe112 100644 --- a/packages/core/test/utils-hoist/cookie.test.ts +++ b/packages/core/test/lib/utils/cookie.test.ts @@ -28,7 +28,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { parseCookie } from '../../src/utils-hoist/cookie'; +import { parseCookie } from '../../../src/utils/cookie'; describe('parseCookie(str)', function () { it('should parse cookie string to object', function () { diff --git a/packages/core/test/lib/utils/hasTracingEnabled.test.ts b/packages/core/test/lib/utils/hasTracingEnabled.test.ts index a03ff25c9be9..a3191336bb1a 100644 --- a/packages/core/test/lib/utils/hasTracingEnabled.test.ts +++ b/packages/core/test/lib/utils/hasTracingEnabled.test.ts @@ -5,24 +5,13 @@ describe('hasTracingEnabled', () => { const tracesSampleRate = 1; it.each([ ['No options', undefined, false], - ['No tracesSampler or tracesSampleRate or enableTracing', {}, false], + ['No tracesSampler or tracesSampleRate', {}, false], ['With tracesSampler', { tracesSampler }, true], ['With tracesSampleRate', { tracesSampleRate }, true], - ['With enableTracing=true', { enableTracing: true }, true], - ['With enableTracing=false', { enableTracing: false }, false], - ['With tracesSampler && enableTracing=false', { tracesSampler, enableTracing: false }, true], - ['With tracesSampleRate && enableTracing=false', { tracesSampler, enableTracing: false }, true], + ['With tracesSampleRate=undefined', { tracesSampleRate: undefined }, false], + ['With tracesSampleRate=0', { tracesSampleRate: 0 }, true], + ['With tracesSampler=undefined', { tracesSampler: undefined }, false], ['With tracesSampler and tracesSampleRate', { tracesSampler, tracesSampleRate }, true], - [ - 'With tracesSampler and tracesSampleRate and enableTracing=true', - { tracesSampler, tracesSampleRate, enableTracing: true }, - true, - ], - [ - 'With tracesSampler and tracesSampleRate and enableTracing=false', - { tracesSampler, tracesSampleRate, enableTracing: false }, - true, - ], ])( '%s', (_: string, input: Parameters[0], output: ReturnType) => { diff --git a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts index a8d7c43a0784..c20f8bc011fa 100644 --- a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts +++ b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts @@ -1,4 +1,4 @@ -import type { Client } from '../../../src/types-hoist'; +import type { Client } from '../../../src/client'; import { isSentryRequestUrl } from '../../../src'; diff --git a/packages/core/test/lib/utils/request.test.ts b/packages/core/test/lib/utils/request.test.ts new file mode 100644 index 000000000000..f1d62a7f2a73 --- /dev/null +++ b/packages/core/test/lib/utils/request.test.ts @@ -0,0 +1,213 @@ +import { + extractQueryParamsFromUrl, + headersToDict, + httpRequestToRequestData, + winterCGHeadersToDict, + winterCGRequestToRequestData, +} from '../../../src/utils/request'; + +describe('request utils', () => { + describe('winterCGHeadersToDict', () => { + it('works with invalid headers object', () => { + expect(winterCGHeadersToDict({} as any)).toEqual({}); + }); + + it('works with header object', () => { + expect( + winterCGHeadersToDict({ + forEach: (callbackfn: (value: unknown, key: string) => void): void => { + callbackfn('value1', 'key1'); + callbackfn(['value2'], 'key2'); + callbackfn('value3', 'key3'); + }, + } as any), + ).toEqual({ + key1: 'value1', + key3: 'value3', + }); + }); + }); + + describe('headersToDict', () => { + it('works with empty object', () => { + expect(headersToDict({})).toEqual({}); + }); + + it('works with plain object', () => { + expect( + headersToDict({ + key1: 'value1', + key2: ['value2'], + key3: 'value3', + }), + ).toEqual({ + key1: 'value1', + key3: 'value3', + }); + }); + }); + + describe('winterCGRequestToRequestData', () => { + it('works', () => { + const actual = winterCGRequestToRequestData({ + method: 'GET', + url: 'http://example.com?foo=bar&baz=qux', + headers: { + forEach: (callbackfn: (value: unknown, key: string) => void): void => { + callbackfn('value1', 'key1'); + callbackfn(['value2'], 'key2'); + callbackfn('value3', 'key3'); + }, + } as any, + clone: () => ({}) as any, + }); + + expect(actual).toEqual({ + headers: { + key1: 'value1', + key3: 'value3', + }, + method: 'GET', + query_string: 'foo=bar&baz=qux', + url: 'http://example.com?foo=bar&baz=qux', + }); + }); + }); + + describe('httpRequestToRequestData', () => { + it('works with minimal request', () => { + const actual = httpRequestToRequestData({}); + expect(actual).toEqual({ + headers: {}, + }); + }); + + it('works with absolute URL request', () => { + const actual = httpRequestToRequestData({ + method: 'GET', + url: 'http://example.com/blabla?xx=a&yy=z', + headers: { + key1: 'value1', + key2: ['value2'], + key3: 'value3', + }, + }); + + expect(actual).toEqual({ + method: 'GET', + url: 'http://example.com/blabla?xx=a&yy=z', + headers: { + key1: 'value1', + key3: 'value3', + }, + query_string: 'xx=a&yy=z', + }); + }); + + it('works with relative URL request without host', () => { + const actual = httpRequestToRequestData({ + method: 'GET', + url: '/blabla', + headers: { + key1: 'value1', + key2: ['value2'], + key3: 'value3', + }, + }); + + expect(actual).toEqual({ + method: 'GET', + headers: { + key1: 'value1', + key3: 'value3', + }, + }); + }); + + it('works with relative URL request with host', () => { + const actual = httpRequestToRequestData({ + url: '/blabla', + headers: { + host: 'example.com', + }, + }); + + expect(actual).toEqual({ + url: 'http://example.com/blabla', + headers: { + host: 'example.com', + }, + }); + }); + + it('works with relative URL request with host & protocol', () => { + const actual = httpRequestToRequestData({ + url: '/blabla', + headers: { + host: 'example.com', + }, + protocol: 'https', + }); + + expect(actual).toEqual({ + url: 'https://example.com/blabla', + headers: { + host: 'example.com', + }, + }); + }); + + it('works with relative URL request with host & socket', () => { + const actual = httpRequestToRequestData({ + url: '/blabla', + headers: { + host: 'example.com', + }, + socket: { + encrypted: true, + }, + }); + + expect(actual).toEqual({ + url: 'https://example.com/blabla', + headers: { + host: 'example.com', + }, + }); + }); + + it('extracts non-standard cookies', () => { + const actual = httpRequestToRequestData({ + cookies: { xx: 'a', yy: 'z' }, + } as any); + + expect(actual).toEqual({ + headers: {}, + cookies: { xx: 'a', yy: 'z' }, + }); + }); + + it('extracts non-standard body', () => { + const actual = httpRequestToRequestData({ + body: { xx: 'a', yy: 'z' }, + } as any); + + expect(actual).toEqual({ + headers: {}, + data: { xx: 'a', yy: 'z' }, + }); + }); + }); + + describe('extractQueryParamsFromUrl', () => { + it.each([ + ['/', undefined], + ['http://example.com', undefined], + ['/sub-path', undefined], + ['/sub-path?xx=a&yy=z', 'xx=a&yy=z'], + ['http://example.com/sub-path?xx=a&yy=z', 'xx=a&yy=z'], + ])('works with %s', (url, expected) => { + expect(extractQueryParamsFromUrl(url)).toEqual(expected); + }); + }); +}); diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index f7187695a025..8139460e8304 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,6 +1,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, @@ -14,8 +15,14 @@ import { } from '../../../src'; import type { Span, SpanAttributes, SpanStatus, SpanTimeInput } from '../../../src/types-hoist'; import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils'; -import { spanToTraceContext } from '../../../src/utils/spanUtils'; -import { getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils'; +import { + getRootSpan, + spanIsSampled, + spanTimeInputToSeconds, + spanToJSON, + spanToTraceContext, + updateSpanName, +} from '../../../src/utils/spanUtils'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; function createMockedOtelSpan({ @@ -287,10 +294,21 @@ describe('spanToJSON', () => { }); }); - it('returns empty object for unknown span implementation', () => { - const span = { other: 'other' }; - - expect(spanToJSON(span as unknown as Span)).toEqual({}); + it('returns minimal object for unknown span implementation', () => { + const span = { + // This is the minimal interface we require from a span + spanContext: () => ({ + spanId: 'SPAN-1', + traceId: 'TRACE-1', + }), + }; + + expect(spanToJSON(span as unknown as Span)).toEqual({ + span_id: 'SPAN-1', + trace_id: 'TRACE-1', + start_timestamp: 0, + data: {}, + }); }); }); @@ -332,3 +350,13 @@ describe('getRootSpan', () => { }); }); }); + +describe('updateSpanName', () => { + it('updates the span name and source', () => { + const span = new SentrySpan({ name: 'old-name', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }); + updateSpanName(span, 'new-name'); + const spanJSON = spanToJSON(span); + expect(spanJSON.description).toBe('new-name'); + expect(spanJSON.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom'); + }); +}); diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index aad060b462de..78c8d806be2a 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -1,3 +1,4 @@ +import type { Client } from '../../../src/'; import { SentrySpan, getCurrentScope, @@ -11,20 +12,18 @@ import { } from '../../../src/'; import { getAsyncContextStrategy } from '../../../src/asyncContext'; import { freezeDscOnSpan } from '../../../src/tracing/dynamicSamplingContext'; -import type { Client, Span } from '../../../src/types-hoist'; - +import type { Span } from '../../../src/types-hoist'; import type { TestClientOptions } from '../../mocks/client'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; const dsn = 'https://123@sentry.io/42'; const SCOPE_TRACE_ID = '12345678901234567890123456789012'; -const SCOPE_SPAN_ID = '1234567890123456'; function setupClient(opts?: Partial): Client { getCurrentScope().setPropagationContext({ traceId: SCOPE_TRACE_ID, - spanId: SCOPE_SPAN_ID, + sampleRand: Math.random(), }); const options = getDefaultTestClientOptions({ @@ -164,20 +163,22 @@ describe('getTraceData', () => { getCurrentScope().setPropagationContext({ traceId: '12345678901234567890123456789012', sampled: true, - spanId: '1234567890123456', + parentSpanId: '1234567890123456', + sampleRand: 0.42, dsc: { environment: 'staging', public_key: 'key', trace_id: '12345678901234567890123456789012', + sample_rand: '0.42', }, }); const traceData = getTraceData(); - expect(traceData).toEqual({ - 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', - baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012', - }); + expect(traceData['sentry-trace']).toMatch(/^12345678901234567890123456789012-[a-f0-9]{16}-1$/); + expect(traceData.baggage).toEqual( + 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012,sentry-sample_rand=0.42', + ); }); it('returns frozen DSC from SentrySpan if available', () => { diff --git a/packages/core/test/lib/utils/transactionEvent.test.ts b/packages/core/test/lib/utils/transactionEvent.test.ts new file mode 100644 index 000000000000..cd5a3cd750c5 --- /dev/null +++ b/packages/core/test/lib/utils/transactionEvent.test.ts @@ -0,0 +1,170 @@ +import { SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, SEMANTIC_ATTRIBUTE_PROFILE_ID } from '../../../src/semanticAttributes'; +import type { SpanJSON, TransactionEvent } from '../../../src/types-hoist'; +import {} from '../../../src/types-hoist'; +import { + convertSpanJsonToTransactionEvent, + convertTransactionEventToSpanJson, +} from '../../../src/utils/transactionEvent'; + +describe('convertTransactionEventToSpanJson', () => { + it('should convert a minimal transaction event to span JSON', () => { + const event: TransactionEvent = { + type: 'transaction', + contexts: { + trace: { + trace_id: 'abc123', + span_id: 'span456', + }, + }, + timestamp: 1234567890, + }; + + expect(convertTransactionEventToSpanJson(event)).toEqual({ + data: {}, + span_id: 'span456', + start_timestamp: 0, + timestamp: 1234567890, + trace_id: 'abc123', + is_segment: true, + }); + }); + + it('should convert a full transaction event to span JSON', () => { + const event: TransactionEvent = { + type: 'transaction', + transaction: 'Test Transaction', + contexts: { + trace: { + trace_id: 'abc123', + parent_span_id: 'parent789', + span_id: 'span456', + status: 'ok', + origin: 'manual', + op: 'http', + data: { + [SEMANTIC_ATTRIBUTE_PROFILE_ID]: 'profile123', + [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: 123.45, + other: 'value', + }, + }, + }, + start_timestamp: 1234567800, + timestamp: 1234567890, + measurements: { + fp: { value: 123, unit: 'millisecond' }, + }, + }; + + expect(convertTransactionEventToSpanJson(event)).toEqual({ + data: { + [SEMANTIC_ATTRIBUTE_PROFILE_ID]: 'profile123', + [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: 123.45, + other: 'value', + }, + description: 'Test Transaction', + op: 'http', + parent_span_id: 'parent789', + span_id: 'span456', + start_timestamp: 1234567800, + status: 'ok', + timestamp: 1234567890, + trace_id: 'abc123', + origin: 'manual', + profile_id: 'profile123', + exclusive_time: 123.45, + measurements: { + fp: { value: 123, unit: 'millisecond' }, + }, + is_segment: true, + }); + }); + + it('should handle missing contexts.trace', () => { + const event: TransactionEvent = { + type: 'transaction', + contexts: {}, + }; + + expect(convertTransactionEventToSpanJson(event)).toEqual({ + data: {}, + span_id: '', + start_timestamp: 0, + trace_id: '', + is_segment: true, + }); + }); +}); + +describe('convertSpanJsonToTransactionEvent', () => { + it('should convert a minimal span JSON to transaction event', () => { + const span: SpanJSON = { + data: {}, + parent_span_id: '', + span_id: 'span456', + start_timestamp: 0, + timestamp: 1234567890, + trace_id: 'abc123', + }; + + expect(convertSpanJsonToTransactionEvent(span)).toEqual({ + type: 'transaction', + timestamp: 1234567890, + start_timestamp: 0, + contexts: { + trace: { + trace_id: 'abc123', + span_id: 'span456', + parent_span_id: '', + data: {}, + }, + }, + }); + }); + + it('should convert a full span JSON to transaction event', () => { + const span: SpanJSON = { + data: { + other: 'value', + }, + description: 'Test Transaction', + op: 'http', + parent_span_id: 'parent789', + span_id: 'span456', + start_timestamp: 1234567800, + status: 'ok', + timestamp: 1234567890, + trace_id: 'abc123', + origin: 'manual', + profile_id: 'profile123', + exclusive_time: 123.45, + measurements: { + fp: { value: 123, unit: 'millisecond' }, + }, + }; + + expect(convertSpanJsonToTransactionEvent(span)).toEqual({ + type: 'transaction', + timestamp: 1234567890, + start_timestamp: 1234567800, + transaction: 'Test Transaction', + contexts: { + trace: { + trace_id: 'abc123', + span_id: 'span456', + parent_span_id: 'parent789', + op: 'http', + status: 'ok', + origin: 'manual', + data: { + other: 'value', + [SEMANTIC_ATTRIBUTE_PROFILE_ID]: 'profile123', + [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: 123.45, + }, + }, + }, + measurements: { + fp: { value: 123, unit: 'millisecond' }, + }, + }); + }); +}); diff --git a/packages/core/test/lib/vendor/getClientIpAddress.test.ts b/packages/core/test/lib/vendor/getClientIpAddress.test.ts new file mode 100644 index 000000000000..91c5b1961485 --- /dev/null +++ b/packages/core/test/lib/vendor/getClientIpAddress.test.ts @@ -0,0 +1,31 @@ +import { getClientIPAddress } from '../../../src/vendor/getIpAddress'; + +describe('getClientIPAddress', () => { + it.each([ + [ + '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5,2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35', + '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', + ], + [ + '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35', + '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', + ], + [ + '2a01:cb19:8350:ed00:d0dd:INVALID_IP_ADDR:8be5,141.101.69.35,2a01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', + '141.101.69.35', + ], + [ + '2b01:cb19:8350:ed00:d0dd:fa5b:nope:8be5, 2b01:cb19:NOPE:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35 ', + '141.101.69.35', + ], + ['2b01:cb19:8350:ed00:d0 dd:fa5b:de31:8be5, 141.101.69.35', '141.101.69.35'], + ])('should parse the IP from the X-Forwarded-For header %s', (headerValue, expectedIP) => { + const headers = { + 'X-Forwarded-For': headerValue, + }; + + const ip = getClientIPAddress(headers); + + expect(ip).toEqual(expectedIP); + }); +}); diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index eaf06909c9ab..eee8a1a6dfe4 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -9,7 +9,7 @@ import type { SeverityLevel, } from '../../src/types-hoist'; -import { BaseClient } from '../../src/baseclient'; +import { Client } from '../../src/client'; import { initAndBind } from '../../src/sdk'; import { createTransport } from '../../src/transports/base'; import { resolvedSyncPromise } from '../../src/utils-hoist/syncpromise'; @@ -37,7 +37,7 @@ export interface TestClientOptions extends ClientOptions { defaultIntegrations?: Integration[] | false; } -export class TestClient extends BaseClient { +export class TestClient extends Client { public static instance?: TestClient; public static sendEventCalled?: (event: Event) => void; @@ -85,7 +85,7 @@ export class TestClient extends BaseClient { // In real life, this will get deleted as part of envelope creation. delete event.sdkProcessingMetadata; - TestClient.sendEventCalled && TestClient.sendEventCalled(event); + TestClient.sendEventCalled?.(event); } public sendSession(session: Session): void { diff --git a/packages/core/test/mocks/integration.ts b/packages/core/test/mocks/integration.ts index 5028bfa71ac7..d9c031ee927b 100644 --- a/packages/core/test/mocks/integration.ts +++ b/packages/core/test/mocks/integration.ts @@ -1,6 +1,6 @@ -import type { Client, Event, EventProcessor, Integration } from '../../src/types-hoist'; - +import type { Client } from '../../src'; import { getClient, getCurrentScope } from '../../src'; +import type { Event, EventProcessor, Integration } from '../../src/types-hoist'; export class TestIntegration implements Integration { public static id: string = 'TestIntegration'; diff --git a/packages/core/test/utils-hoist/array.test.ts b/packages/core/test/utils-hoist/array.test.ts deleted file mode 100644 index 3716a6d190b1..000000000000 --- a/packages/core/test/utils-hoist/array.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { NestedArray } from '../../src/utils-hoist/array'; -// eslint-disable-next-line deprecation/deprecation -import { flatten } from '../../src/utils-hoist/array'; - -describe('flatten', () => { - it('should return the same array when input is a flat array', () => { - const input = [1, 2, 3, 4]; - const expected = [1, 2, 3, 4]; - // eslint-disable-next-line deprecation/deprecation - expect(flatten(input)).toEqual(expected); - }); - - it('should flatten a nested array of numbers', () => { - const input = [[1, 2, [3]], 4]; - const expected = [1, 2, 3, 4]; - // eslint-disable-next-line deprecation/deprecation - expect(flatten(input)).toEqual(expected); - }); - - it('should flatten a nested array of strings', () => { - const input = [ - ['Hello', 'World'], - ['How', 'Are', 'You'], - ]; - const expected = ['Hello', 'World', 'How', 'Are', 'You']; - // eslint-disable-next-line deprecation/deprecation - expect(flatten(input)).toEqual(expected); - }); - - it('should flatten a nested array of objects', () => { - const input: NestedArray<{ a: number; b?: number } | { b: number; a?: number }> = [ - [{ a: 1 }, { b: 2 }], - [{ a: 3 }, { b: 4 }], - ]; - const expected = [{ a: 1 }, { b: 2 }, { a: 3 }, { b: 4 }]; - // eslint-disable-next-line deprecation/deprecation - expect(flatten(input)).toEqual(expected); - }); - - it('should flatten a mixed type array', () => { - const input: NestedArray = [['a', { b: 2 }, 'c'], 'd']; - const expected = ['a', { b: 2 }, 'c', 'd']; - // eslint-disable-next-line deprecation/deprecation - expect(flatten(input)).toEqual(expected); - }); - - it('should flatten a deeply nested array', () => { - const input = [1, [2, [3, [4, [5]]]]]; - const expected = [1, 2, 3, 4, 5]; - // eslint-disable-next-line deprecation/deprecation - expect(flatten(input)).toEqual(expected); - }); - - it('should return an empty array when input is empty', () => { - const input: any[] = []; - const expected: any[] = []; - // eslint-disable-next-line deprecation/deprecation - expect(flatten(input)).toEqual(expected); - }); - - it('should return the same array when input is a flat array', () => { - const input = [1, 'a', { b: 2 }, 'c', 3]; - const expected = [1, 'a', { b: 2 }, 'c', 3]; - // eslint-disable-next-line deprecation/deprecation - expect(flatten(input)).toEqual(expected); - }); -}); diff --git a/packages/core/test/utils-hoist/browser.test.ts b/packages/core/test/utils-hoist/browser.test.ts index c86570ee7fb0..01a0d5eee747 100644 --- a/packages/core/test/utils-hoist/browser.test.ts +++ b/packages/core/test/utils-hoist/browser.test.ts @@ -1,6 +1,6 @@ import { JSDOM } from 'jsdom'; -import { getDomElement, htmlTreeAsString } from '../../src/utils-hoist/browser'; +import { htmlTreeAsString } from '../../src/utils-hoist/browser'; beforeAll(() => { const dom = new JSDOM(); @@ -74,13 +74,3 @@ describe('htmlTreeAsString', () => { ); }); }); - -describe('getDomElement', () => { - it('returns the element for a given query selector', () => { - document.head.innerHTML = '
Hello
'; - const el = getDomElement('div#mydiv'); - expect(el).toBeDefined(); - expect(el?.tagName).toEqual('DIV'); - expect(el?.id).toEqual('mydiv'); - }); -}); diff --git a/packages/core/test/utils-hoist/buildPolyfills/nullishCoalesce.test.ts b/packages/core/test/utils-hoist/buildPolyfills/nullishCoalesce.test.ts deleted file mode 100644 index 11c83b0711d9..000000000000 --- a/packages/core/test/utils-hoist/buildPolyfills/nullishCoalesce.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -// TODO(v9): Remove this test - -import { _nullishCoalesce } from '../../../src/utils-hoist/buildPolyfills'; -import type { Value } from '../../../src/utils-hoist/buildPolyfills/types'; -import { _nullishCoalesce as _nullishCoalesceOrig } from './originals'; - -const dogStr = 'dogs are great!'; -const dogFunc = () => dogStr; -const dogAdjectives = { maisey: 'silly', charlie: 'goofy' }; -const dogAdjectiveFunc = () => dogAdjectives; - -describe('_nullishCoalesce', () => { - describe('returns the same result as the original', () => { - const testCases: Array<[string, Value, () => Value, Value]> = [ - ['null LHS', null, dogFunc, dogStr], - ['undefined LHS', undefined, dogFunc, dogStr], - ['false LHS', false, dogFunc, false], - ['zero LHS', 0, dogFunc, 0], - ['empty string LHS', '', dogFunc, ''], - ['true LHS', true, dogFunc, true], - ['truthy primitive LHS', 12312012, dogFunc, 12312012], - ['truthy object LHS', dogAdjectives, dogFunc, dogAdjectives], - ['truthy function LHS', dogAdjectiveFunc, dogFunc, dogAdjectiveFunc], - ]; - - it.each(testCases)('%s', (_, lhs, rhs, expectedValue) => { - expect(_nullishCoalesce(lhs, rhs)).toEqual(_nullishCoalesceOrig(lhs, rhs)); - expect(_nullishCoalesce(lhs, rhs)).toEqual(expectedValue); - }); - }); -}); diff --git a/packages/core/test/utils-hoist/buildPolyfills/optionalChain.test.ts b/packages/core/test/utils-hoist/buildPolyfills/optionalChain.test.ts deleted file mode 100644 index bd7a7bb052fb..000000000000 --- a/packages/core/test/utils-hoist/buildPolyfills/optionalChain.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -// TODO(v9): Remove this test - -import { shim as arrayFlatShim } from 'array.prototype.flat'; - -import { _optionalChain } from '../../../src/utils-hoist/buildPolyfills'; -import type { GenericFunction, GenericObject, Value } from '../../../src/utils-hoist/buildPolyfills/types'; -import { _optionalChain as _optionalChainOrig } from './originals'; - -// Older versions of Node don't have `Array.prototype.flat`, which crashes these tests. On newer versions that do have -// it, this is a no-op. -arrayFlatShim(); - -type OperationType = 'access' | 'call' | 'optionalAccess' | 'optionalCall'; -type OperationExecutor = - | ((intermediateValue: GenericObject) => Value) - | ((intermediateValue: GenericFunction) => Value); -type Operation = [OperationType, OperationExecutor]; - -const truthyObject = { maisey: 'silly', charlie: 'goofy' }; -const nullishObject = null; -const truthyFunc = (): GenericObject => truthyObject; -const nullishFunc = undefined; -const truthyReturn = (): GenericObject => truthyObject; -const nullishReturn = (): null => nullishObject; - -// The polyfill being tested here works under the assumption that the original code containing the optional chain has -// been transformed into an array of values, labels, and functions. For example, `truthyObject?.charlie` will have been -// transformed into `_optionalChain([truthyObject, 'optionalAccess', _ => _.charlie])`. We are not testing the -// transformation here, only what the polyfill does with the already-transformed inputs. - -describe('_optionalChain', () => { - describe('returns the same result as the original', () => { - // In these test cases, the array passed to `_optionalChain` has been broken up into the first entry followed by an - // array of pairs of subsequent elements, because this seemed the easiest way to express the type, which is really - // - // [Value, OperationType, Value => Value, OperationType, Value => Value, OperationType, Value => Value, ...]. - // - // (In other words, `[A, B, C, D, E]` has become `A, [[B, C], [D, E]]`, and these are then the second and third - // entries in each test case.) We then undo this wrapping before passing the data to our functions. - const testCases: Array<[string, Value, Operation[], Value]> = [ - ['truthyObject?.charlie', truthyObject, [['optionalAccess', (_: GenericObject) => _.charlie]], 'goofy'], - ['nullishObject?.maisey', nullishObject, [['optionalAccess', (_: GenericObject) => _.maisey]], undefined], - [ - 'truthyFunc?.().maisey', - truthyFunc, - [ - ['optionalCall', (_: GenericFunction) => _()], - ['access', (_: GenericObject) => _.maisey], - ], - 'silly', - ], - [ - 'nullishFunc?.().charlie', - nullishFunc, - [ - ['optionalCall', (_: GenericFunction) => _()], - ['access', (_: GenericObject) => _.charlie], - ], - undefined, - ], - [ - 'truthyReturn()?.maisey', - truthyReturn, - [ - ['call', (_: GenericFunction) => _()], - ['optionalAccess', (_: GenericObject) => _.maisey], - ], - 'silly', - ], - [ - 'nullishReturn()?.charlie', - nullishReturn, - [ - ['call', (_: GenericFunction) => _()], - ['optionalAccess', (_: GenericObject) => _.charlie], - ], - undefined, - ], - ]; - - it.each(testCases)('%s', (_, initialChainComponent, operations, expectedValue) => { - // `operations` is flattened and spread in order to undo the wrapping done in the test cases for TS purposes. - // @ts-expect-error this is what we're testing - expect(_optionalChain([initialChainComponent, ...operations.flat()])).toEqual( - // @ts-expect-error this is what we're testing - _optionalChainOrig([initialChainComponent, ...operations.flat()]), - ); - // @ts-expect-error this is what we're testing - expect(_optionalChain([initialChainComponent, ...operations.flat()])).toEqual(expectedValue); - }); - }); -}); diff --git a/packages/core/test/utils-hoist/buildPolyfills/originals.ts b/packages/core/test/utils-hoist/buildPolyfills/originals.ts deleted file mode 100644 index a504d5f0d871..000000000000 --- a/packages/core/test/utils-hoist/buildPolyfills/originals.ts +++ /dev/null @@ -1,85 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck this is just used for tests - -// TODO(v9): Remove this file - -// Originals of the buildPolyfills from Sucrase and Rollup we use (which we have adapted in various ways), preserved here for testing, to prove that -// the modified versions do the same thing the originals do. - -// From Sucrase -export function _asyncNullishCoalesce(lhs, rhsFn) { - if (lhs != null) { - return lhs; - } else { - return rhsFn(); - } -} - -// From Sucrase -export async function _asyncOptionalChain(ops) { - let lastAccessLHS = undefined; - let value = ops[0]; - let i = 1; - while (i < ops.length) { - const op = ops[i]; - const fn = ops[i + 1]; - i += 2; - if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { - return undefined; - } - if (op === 'access' || op === 'optionalAccess') { - lastAccessLHS = value; - value = await fn(value); - } else if (op === 'call' || op === 'optionalCall') { - value = await fn((...args) => value.call(lastAccessLHS, ...args)); - lastAccessLHS = undefined; - } - } - return value; -} - -// From Sucrase -export async function _asyncOptionalChainDelete(ops) { - const result = await _asyncOptionalChain(ops); - // by checking for loose equality to `null`, we catch both `null` and `undefined` - return result == null ? true : result; -} - -// From Sucrase -export function _nullishCoalesce(lhs, rhsFn) { - if (lhs != null) { - return lhs; - } else { - return rhsFn(); - } -} - -// From Sucrase -export function _optionalChain(ops) { - let lastAccessLHS = undefined; - let value = ops[0]; - let i = 1; - while (i < ops.length) { - const op = ops[i]; - const fn = ops[i + 1]; - i += 2; - if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { - return undefined; - } - if (op === 'access' || op === 'optionalAccess') { - lastAccessLHS = value; - value = fn(value); - } else if (op === 'call' || op === 'optionalCall') { - value = fn((...args) => value.call(lastAccessLHS, ...args)); - lastAccessLHS = undefined; - } - } - return value; -} - -// From Sucrase -export function _optionalChainDelete(ops) { - const result = _optionalChain(ops); - // by checking for loose equality to `null`, we catch both `null` and `undefined` - return result == null ? true : result; -} diff --git a/packages/core/test/utils-hoist/envelope.test.ts b/packages/core/test/utils-hoist/envelope.test.ts index dfb24f043fe8..6da22c1869a9 100644 --- a/packages/core/test/utils-hoist/envelope.test.ts +++ b/packages/core/test/utils-hoist/envelope.test.ts @@ -7,6 +7,7 @@ import { spanToJSON, } from '@sentry/core'; import { SentrySpan } from '@sentry/core'; +import { getSentryCarrier } from '../../src/carrier'; import { addItemToEnvelope, createEnvelope, @@ -71,7 +72,7 @@ describe('envelope', () => { measurements: { inp: { value: expect.any(Number), unit: expect.any(String) } }, }; - expect(spanEnvelopeItem[0]?.type).toBe('span'); + expect(spanEnvelopeItem[0].type).toBe('span'); expect(spanEnvelopeItem[1]).toMatchObject(expectedObj); }); }); @@ -107,17 +108,18 @@ describe('envelope', () => { { name: 'with TextEncoder/Decoder polyfill', before: () => { - GLOBAL_OBJ.__SENTRY__ = {} as InternalGlobal['__SENTRY__']; - GLOBAL_OBJ.__SENTRY__.encodePolyfill = jest.fn((input: string) => + GLOBAL_OBJ.__SENTRY__ = {}; + + getSentryCarrier(GLOBAL_OBJ).encodePolyfill = jest.fn((input: string) => new TextEncoder().encode(input), ); - GLOBAL_OBJ.__SENTRY__.decodePolyfill = jest.fn((input: Uint8Array) => + getSentryCarrier(GLOBAL_OBJ).decodePolyfill = jest.fn((input: Uint8Array) => new TextDecoder().decode(input), ); }, after: () => { - expect(GLOBAL_OBJ.__SENTRY__.encodePolyfill).toHaveBeenCalled(); - expect(GLOBAL_OBJ.__SENTRY__.decodePolyfill).toHaveBeenCalled(); + expect(getSentryCarrier(GLOBAL_OBJ).encodePolyfill).toHaveBeenCalled(); + expect(getSentryCarrier(GLOBAL_OBJ).decodePolyfill).toHaveBeenCalled(); }, }, { diff --git a/packages/core/test/utils-hoist/eventbuilder.test.ts b/packages/core/test/utils-hoist/eventbuilder.test.ts index 2aea3b6192d9..2994fb5520f6 100644 --- a/packages/core/test/utils-hoist/eventbuilder.test.ts +++ b/packages/core/test/utils-hoist/eventbuilder.test.ts @@ -1,4 +1,4 @@ -import type { Client } from '../../src/types-hoist'; +import type { Client } from '../../src/client'; import { eventFromMessage, eventFromUnknownInput } from '../../src/utils-hoist/eventbuilder'; import { nodeStackLineParser } from '../../src/utils-hoist/node-stack-trace'; import { createStackParser } from '../../src/utils-hoist/stacktrace'; diff --git a/packages/core/test/utils-hoist/misc.test.ts b/packages/core/test/utils-hoist/misc.test.ts index a042215fbcb9..05f8736bc443 100644 --- a/packages/core/test/utils-hoist/misc.test.ts +++ b/packages/core/test/utils-hoist/misc.test.ts @@ -3,7 +3,6 @@ import type { Event, Mechanism, StackFrame } from '../../src/types-hoist'; import { addContextToFrame, addExceptionMechanism, - arrayify, checkOrSetAlreadyCaught, getEventDescription, uuid4, @@ -215,7 +214,7 @@ describe('addExceptionMechanism', () => { addExceptionMechanism(event); - expect(event.exception.values[0]?.mechanism).toEqual(defaultMechanism); + expect(event.exception.values[0].mechanism).toEqual(defaultMechanism); }); it('prefers current values to defaults', () => { @@ -226,7 +225,7 @@ describe('addExceptionMechanism', () => { addExceptionMechanism(event); - expect(event.exception.values[0]?.mechanism).toEqual(nonDefaultMechanism); + expect(event.exception.values[0].mechanism).toEqual(nonDefaultMechanism); }); it('prefers incoming values to current values', () => { @@ -239,7 +238,7 @@ describe('addExceptionMechanism', () => { addExceptionMechanism(event, newMechanism); // the new `handled` value took precedence - expect(event.exception.values[0]?.mechanism).toEqual({ type: 'instrument', handled: true, synthetic: true }); + expect(event.exception.values[0].mechanism).toEqual({ type: 'instrument', handled: true, synthetic: true }); }); it('merges data values', () => { @@ -251,7 +250,7 @@ describe('addExceptionMechanism', () => { addExceptionMechanism(event, newMechanism); - expect(event.exception.values[0]?.mechanism.data).toEqual({ + expect(event.exception.values[0].mechanism.data).toEqual({ function: 'addEventListener', handler: 'organizeShoes', target: 'closet', @@ -363,27 +362,3 @@ describe('uuid4 generation', () => { } }); }); - -describe('arrayify()', () => { - it('returns arrays untouched', () => { - // eslint-disable-next-line deprecation/deprecation - expect(arrayify([])).toEqual([]); - // eslint-disable-next-line deprecation/deprecation - expect(arrayify(['dogs', 'are', 'great'])).toEqual(['dogs', 'are', 'great']); - }); - - it('wraps non-arrays with an array', () => { - // eslint-disable-next-line deprecation/deprecation - expect(arrayify(1231)).toEqual([1231]); - // eslint-disable-next-line deprecation/deprecation - expect(arrayify('dogs are great')).toEqual(['dogs are great']); - // eslint-disable-next-line deprecation/deprecation - expect(arrayify(true)).toEqual([true]); - // eslint-disable-next-line deprecation/deprecation - expect(arrayify({})).toEqual([{}]); - // eslint-disable-next-line deprecation/deprecation - expect(arrayify(null)).toEqual([null]); - // eslint-disable-next-line deprecation/deprecation - expect(arrayify(undefined)).toEqual([undefined]); - }); -}); diff --git a/packages/core/test/utils-hoist/object.test.ts b/packages/core/test/utils-hoist/object.test.ts index ed83b5d87aef..6ccfd71f2218 100644 --- a/packages/core/test/utils-hoist/object.test.ts +++ b/packages/core/test/utils-hoist/object.test.ts @@ -11,7 +11,6 @@ import { fill, markFunctionWrapped, objectify, - urlEncode, } from '../../src/utils-hoist/object'; import { testOnlyIfNodeVersionAtLeast } from './testutils'; @@ -128,23 +127,6 @@ describe('fill()', () => { }); }); -describe('urlEncode()', () => { - test('returns empty string for empty object input', () => { - // eslint-disable-next-line deprecation/deprecation - expect(urlEncode({})).toEqual(''); - }); - - test('returns single key/value pair joined with = sign', () => { - // eslint-disable-next-line deprecation/deprecation - expect(urlEncode({ foo: 'bar' })).toEqual('foo=bar'); - }); - - test('returns multiple key/value pairs joined together with & sign', () => { - // eslint-disable-next-line deprecation/deprecation - expect(urlEncode({ foo: 'bar', pickle: 'rick', morty: '4 2' })).toEqual('foo=bar&pickle=rick&morty=4%202'); - }); -}); - describe('extractExceptionKeysForMessage()', () => { test('no keys', () => { expect(extractExceptionKeysForMessage({}, 10)).toEqual('[object has no keys]'); @@ -444,7 +426,8 @@ describe('markFunctionWrapped', () => { const wrappedFunc = jest.fn(); markFunctionWrapped(wrappedFunc, originalFunc); - expect((wrappedFunc as WrappedFunction).__sentry_original__).toBe(originalFunc); + // cannot wrap because it is frozen, but we do not error! + expect((wrappedFunc as WrappedFunction).__sentry_original__).toBe(undefined); wrappedFunc(); diff --git a/packages/core/test/utils-hoist/proagationContext.test.ts b/packages/core/test/utils-hoist/proagationContext.test.ts deleted file mode 100644 index 700e9d0b7942..000000000000 --- a/packages/core/test/utils-hoist/proagationContext.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { generatePropagationContext } from '../../src/utils-hoist/propagationContext'; - -describe('generatePropagationContext', () => { - it('generates a new minimal propagation context', () => { - // eslint-disable-next-line deprecation/deprecation - expect(generatePropagationContext()).toEqual({ - traceId: expect.stringMatching(/^[0-9a-f]{32}$/), - spanId: expect.stringMatching(/^[0-9a-f]{16}$/), - }); - }); -}); diff --git a/packages/core/test/utils-hoist/requestdata.test.ts b/packages/core/test/utils-hoist/requestdata.test.ts deleted file mode 100644 index a36a0669dc7b..000000000000 --- a/packages/core/test/utils-hoist/requestdata.test.ts +++ /dev/null @@ -1,762 +0,0 @@ -/* eslint-disable deprecation/deprecation */ -import type * as net from 'net'; -import { addRequestDataToEvent, extractPathForTransaction, extractRequestData } from '@sentry/core'; -import type { Event, PolymorphicRequest, TransactionSource, User } from '../../src/types-hoist'; -import { getClientIPAddress } from '../../src/utils-hoist/vendor/getIpAddress'; - -describe('addRequestDataToEvent', () => { - let mockEvent: Event; - let mockReq: { [key: string]: any }; - - beforeEach(() => { - mockEvent = {}; - mockReq = { - baseUrl: '/routerMountPath', - body: 'foo', - cookies: { test: 'test' }, - headers: { - host: 'example.org', - }, - method: 'POST', - originalUrl: '/routerMountPath/subpath/specificValue?querystringKey=querystringValue', - path: '/subpath/specificValue', - query: { - querystringKey: 'querystringValue', - }, - route: { - path: '/subpath/:parameterName', - stack: [ - { - name: 'parameterNameRouteHandler', - }, - ], - }, - url: '/subpath/specificValue?querystringKey=querystringValue', - user: { - custom_property: 'foo', - email: 'tobias@mail.com', - id: 123, - username: 'tobias', - }, - }; - }); - - describe('addRequestDataToEvent user properties', () => { - const DEFAULT_USER_KEYS = ['id', 'username', 'email']; - const CUSTOM_USER_KEYS = ['custom_property']; - - test('user only contains the default properties from the user', () => { - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq); - expect(Object.keys(parsedRequest.user as User)).toEqual(DEFAULT_USER_KEYS); - }); - - test('user only contains the custom properties specified in the options.user array', () => { - const optionsWithCustomUserKeys = { - include: { - user: CUSTOM_USER_KEYS, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq, optionsWithCustomUserKeys); - - expect(Object.keys(parsedRequest.user as User)).toEqual(CUSTOM_USER_KEYS); - }); - - test('setting user doesnt blow up when someone passes non-object value', () => { - const reqWithUser = { - ...mockReq, - // intentionally setting user to a non-object value, hence the as any cast - user: 'wat', - } as any; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithUser); - - expect(parsedRequest.user).toBeUndefined(); - }); - }); - - describe('addRequestDataToEvent ip property', () => { - test('can be extracted from req.ip', () => { - const mockReqWithIP = { - ...mockReq, - ip: '123', - }; - const optionsWithIP = { - include: { - ip: true, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReqWithIP, optionsWithIP); - - expect(parsedRequest.user!.ip_address).toEqual('123'); - }); - - test('can extract from req.socket.remoteAddress', () => { - const reqWithIPInSocket = { - ...mockReq, - socket: { - remoteAddress: '321', - } as net.Socket, - }; - const optionsWithIP = { - include: { - ip: true, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInSocket, optionsWithIP); - - expect(parsedRequest.user!.ip_address).toEqual('321'); - }); - - test.each([ - 'X-Client-IP', - 'X-Forwarded-For', - 'Fly-Client-IP', - 'CF-Connecting-IP', - 'Fastly-Client-Ip', - 'True-Client-Ip', - 'X-Real-IP', - 'X-Cluster-Client-IP', - 'X-Forwarded', - 'Forwarded-For', - 'X-Vercel-Forwarded-For', - ])('can be extracted from %s header', headerName => { - const reqWithIPInHeader = { - ...mockReq, - headers: { - [headerName]: '123.5.6.1', - }, - }; - - const optionsWithIP = { - include: { - ip: true, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithIP); - - expect(parsedRequest.user!.ip_address).toEqual('123.5.6.1'); - }); - - it('can be extracted from Forwarded header', () => { - const reqWithIPInHeader = { - ...mockReq, - headers: { - Forwarded: 'by=111;for=123.5.6.1;for=123.5.6.2;', - }, - }; - - const optionsWithIP = { - include: { - ip: true, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithIP); - - expect(parsedRequest.user!.ip_address).toEqual('123.5.6.1'); - }); - - test('it ignores invalid IP in header', () => { - const reqWithIPInHeader = { - ...mockReq, - headers: { - 'X-Client-IP': 'invalid', - }, - }; - - const optionsWithIP = { - include: { - ip: true, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithIP); - - expect(parsedRequest.user!.ip_address).toEqual(undefined); - }); - - test('IP from header takes presedence over socket', () => { - const reqWithIPInHeader = { - ...mockReq, - headers: { - 'X-Client-IP': '123.5.6.1', - }, - socket: { - remoteAddress: '321', - } as net.Socket, - }; - - const optionsWithIP = { - include: { - ip: true, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithIP); - - expect(parsedRequest.user!.ip_address).toEqual('123.5.6.1'); - }); - - test('IP from header takes presedence over req.ip', () => { - const reqWithIPInHeader = { - ...mockReq, - headers: { - 'X-Client-IP': '123.5.6.1', - }, - ip: '123', - }; - - const optionsWithIP = { - include: { - ip: true, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithIP); - - expect(parsedRequest.user!.ip_address).toEqual('123.5.6.1'); - }); - - test('does not add IP if ip=false', () => { - const reqWithIPInHeader = { - ...mockReq, - headers: { - 'X-Client-IP': '123.5.6.1', - }, - ip: '123', - }; - - const optionsWithoutIP = { - include: { - ip: false, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithoutIP); - - expect(parsedRequest.user!.ip_address).toEqual(undefined); - }); - - test('does not add IP by default', () => { - const reqWithIPInHeader = { - ...mockReq, - headers: { - 'X-Client-IP': '123.5.6.1', - }, - ip: '123', - }; - - const optionsWithoutIP = {}; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithoutIP); - - expect(parsedRequest.user!.ip_address).toEqual(undefined); - }); - - test('removes IP headers if `ip` is not set in the options', () => { - const reqWithIPInHeader = { - ...mockReq, - headers: { - otherHeader: 'hello', - 'X-Client-IP': '123', - 'X-Forwarded-For': '123', - 'Fly-Client-IP': '123', - 'CF-Connecting-IP': '123', - 'Fastly-Client-Ip': '123', - 'True-Client-Ip': '123', - 'X-Real-IP': '123', - 'X-Cluster-Client-IP': '123', - 'X-Forwarded': '123', - 'Forwarded-For': '123', - Forwarded: '123', - 'X-Vercel-Forwarded-For': '123', - }, - }; - - const optionsWithoutIP = { - include: {}, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithoutIP); - - expect(parsedRequest.request?.headers).toEqual({ otherHeader: 'hello' }); - }); - - test('keeps IP headers if `ip=true`', () => { - const reqWithIPInHeader = { - ...mockReq, - headers: { - otherHeader: 'hello', - 'X-Client-IP': '123', - 'X-Forwarded-For': '123', - 'Fly-Client-IP': '123', - 'CF-Connecting-IP': '123', - 'Fastly-Client-Ip': '123', - 'True-Client-Ip': '123', - 'X-Real-IP': '123', - 'X-Cluster-Client-IP': '123', - 'X-Forwarded': '123', - 'Forwarded-For': '123', - Forwarded: '123', - 'X-Vercel-Forwarded-For': '123', - }, - }; - - const optionsWithoutIP = { - include: { - ip: true, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithoutIP); - - expect(parsedRequest.request?.headers).toEqual({ - otherHeader: 'hello', - 'X-Client-IP': '123', - 'X-Forwarded-For': '123', - 'Fly-Client-IP': '123', - 'CF-Connecting-IP': '123', - 'Fastly-Client-Ip': '123', - 'True-Client-Ip': '123', - 'X-Real-IP': '123', - 'X-Cluster-Client-IP': '123', - 'X-Forwarded': '123', - 'Forwarded-For': '123', - Forwarded: '123', - 'X-Vercel-Forwarded-For': '123', - }); - }); - }); - - describe('request properties', () => { - test('request only contains the default set of properties from the request', () => { - const DEFAULT_REQUEST_PROPERTIES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq); - - expect(Object.keys(parsedRequest.request!)).toEqual(DEFAULT_REQUEST_PROPERTIES); - }); - - test('request only contains the specified properties in the options.request array', () => { - const INCLUDED_PROPERTIES = ['data', 'headers', 'query_string', 'url']; - const optionsWithRequestIncludes = { - include: { - request: INCLUDED_PROPERTIES, - }, - }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq, optionsWithRequestIncludes); - - expect(Object.keys(parsedRequest.request!)).toEqual(INCLUDED_PROPERTIES); - }); - - test.each([ - [undefined, true], - ['GET', false], - ['HEAD', false], - ])('request skips `body` property for GET and HEAD requests - %s method', (method, shouldIncludeBodyData) => { - const reqWithMethod = { ...mockReq, method }; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithMethod); - - if (shouldIncludeBodyData) { - expect(parsedRequest.request).toHaveProperty('data'); - } else { - expect(parsedRequest.request).not.toHaveProperty('data'); - } - }); - }); -}); - -describe('extractRequestData', () => { - describe('default behaviour', () => { - test('node', () => { - const mockReq = { - headers: { host: 'example.com' }, - method: 'GET', - socket: { encrypted: true }, - originalUrl: '/', - }; - - expect(extractRequestData(mockReq)).toEqual({ - cookies: {}, - headers: { - host: 'example.com', - }, - method: 'GET', - query_string: undefined, - url: 'https://example.com/', - }); - }); - - test('degrades gracefully without request data', () => { - const mockReq = {}; - - expect(extractRequestData(mockReq)).toEqual({ - cookies: {}, - headers: {}, - method: undefined, - query_string: undefined, - url: 'http://', - }); - }); - }); - - describe('headers', () => { - it('removes the `Cookie` header from requestdata.headers, if `cookies` is not set in the options', () => { - const mockReq = { - cookies: { foo: 'bar' }, - headers: { cookie: 'foo=bar', otherHeader: 'hello' }, - }; - const options = { include: ['headers'] }; - - expect(extractRequestData(mockReq, options)).toStrictEqual({ - headers: { otherHeader: 'hello' }, - }); - }); - - it('includes the `Cookie` header in requestdata.headers, if `cookies` is set in the options', () => { - const mockReq = { - cookies: { foo: 'bar' }, - headers: { cookie: 'foo=bar', otherHeader: 'hello' }, - }; - const optionsWithCookies = { include: ['headers', 'cookies'] }; - - expect(extractRequestData(mockReq, optionsWithCookies)).toStrictEqual({ - headers: { otherHeader: 'hello', cookie: 'foo=bar' }, - cookies: { foo: 'bar' }, - }); - }); - - it('removes IP-related headers from requestdata.headers, if `ip` is not set in the options', () => { - const mockReq = { - headers: { - otherHeader: 'hello', - 'X-Client-IP': '123', - 'X-Forwarded-For': '123', - 'Fly-Client-IP': '123', - 'CF-Connecting-IP': '123', - 'Fastly-Client-Ip': '123', - 'True-Client-Ip': '123', - 'X-Real-IP': '123', - 'X-Cluster-Client-IP': '123', - 'X-Forwarded': '123', - 'Forwarded-For': '123', - Forwarded: '123', - 'X-Vercel-Forwarded-For': '123', - }, - }; - const options = { include: ['headers'] }; - - expect(extractRequestData(mockReq, options)).toStrictEqual({ - headers: { otherHeader: 'hello' }, - }); - }); - - it('keeps IP-related headers from requestdata.headers, if `ip` is enabled in options', () => { - const mockReq = { - headers: { - otherHeader: 'hello', - 'X-Client-IP': '123', - 'X-Forwarded-For': '123', - 'Fly-Client-IP': '123', - 'CF-Connecting-IP': '123', - 'Fastly-Client-Ip': '123', - 'True-Client-Ip': '123', - 'X-Real-IP': '123', - 'X-Cluster-Client-IP': '123', - 'X-Forwarded': '123', - 'Forwarded-For': '123', - Forwarded: '123', - 'X-Vercel-Forwarded-For': '123', - }, - }; - const options = { include: ['headers', 'ip'] }; - - expect(extractRequestData(mockReq, options)).toStrictEqual({ - headers: { - otherHeader: 'hello', - 'X-Client-IP': '123', - 'X-Forwarded-For': '123', - 'Fly-Client-IP': '123', - 'CF-Connecting-IP': '123', - 'Fastly-Client-Ip': '123', - 'True-Client-Ip': '123', - 'X-Real-IP': '123', - 'X-Cluster-Client-IP': '123', - 'X-Forwarded': '123', - 'Forwarded-For': '123', - Forwarded: '123', - 'X-Vercel-Forwarded-For': '123', - }, - }); - }); - }); - - describe('cookies', () => { - it('uses `req.cookies` if available', () => { - const mockReq = { - cookies: { foo: 'bar' }, - }; - const optionsWithCookies = { include: ['cookies'] }; - - expect(extractRequestData(mockReq, optionsWithCookies)).toEqual({ - cookies: { foo: 'bar' }, - }); - }); - - it('parses the cookie header', () => { - const mockReq = { - headers: { - cookie: 'foo=bar;', - }, - }; - const optionsWithCookies = { include: ['cookies'] }; - - expect(extractRequestData(mockReq, optionsWithCookies)).toEqual({ - cookies: { foo: 'bar' }, - }); - }); - - it('falls back if no cookies are defined', () => { - const mockReq = {}; - const optionsWithCookies = { include: ['cookies'] }; - - expect(extractRequestData(mockReq, optionsWithCookies)).toEqual({ - cookies: {}, - }); - }); - }); - - describe('data', () => { - it('includes data from `req.body` if available', () => { - const mockReq = { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: 'foo=bar', - }; - const optionsWithData = { include: ['data'] }; - - expect(extractRequestData(mockReq, optionsWithData)).toEqual({ - data: 'foo=bar', - }); - }); - - it('encodes JSON body contents back to a string', () => { - const mockReq = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: { foo: 'bar' }, - }; - const optionsWithData = { include: ['data'] }; - - expect(extractRequestData(mockReq, optionsWithData)).toEqual({ - data: '{"foo":"bar"}', - }); - }); - }); - - describe('query_string', () => { - it('parses the query parms from the url', () => { - const mockReq = { - headers: { host: 'example.com' }, - secure: true, - originalUrl: '/?foo=bar', - }; - const optionsWithQueryString = { include: ['query_string'] }; - - expect(extractRequestData(mockReq, optionsWithQueryString)).toEqual({ - query_string: 'foo=bar', - }); - }); - - it('gracefully degrades if url cannot be determined', () => { - const mockReq = {}; - const optionsWithQueryString = { include: ['query_string'] }; - - expect(extractRequestData(mockReq, optionsWithQueryString)).toEqual({ - query_string: undefined, - }); - }); - }); - - describe('url', () => { - test('express/koa', () => { - const mockReq = { - host: 'example.com', - protocol: 'https', - url: '/', - }; - const optionsWithURL = { include: ['url'] }; - - expect(extractRequestData(mockReq, optionsWithURL)).toEqual({ - url: 'https://example.com/', - }); - }); - - test('node', () => { - const mockReq = { - headers: { host: 'example.com' }, - socket: { encrypted: true }, - originalUrl: '/', - }; - const optionsWithURL = { include: ['url'] }; - - expect(extractRequestData(mockReq, optionsWithURL)).toEqual({ - url: 'https://example.com/', - }); - }); - }); - - describe('custom key', () => { - it('includes the custom key if present', () => { - const mockReq = { - httpVersion: '1.1', - } as any; - const optionsWithCustomKey = { include: ['httpVersion'] }; - - expect(extractRequestData(mockReq, optionsWithCustomKey)).toEqual({ - httpVersion: '1.1', - }); - }); - - it('gracefully degrades if the custom key is missing', () => { - const mockReq = {} as any; - const optionsWithCustomKey = { include: ['httpVersion'] }; - - expect(extractRequestData(mockReq, optionsWithCustomKey)).toEqual({}); - }); - }); -}); - -describe('extractPathForTransaction', () => { - it.each([ - [ - 'extracts a parameterized route and method if available', - { - method: 'get', - baseUrl: '/api/users', - route: { path: '/:id/details' }, - originalUrl: '/api/users/123/details', - } as PolymorphicRequest, - { path: true, method: true }, - 'GET /api/users/:id/details', - 'route' as TransactionSource, - ], - [ - 'ignores the method if specified', - { - method: 'get', - baseUrl: '/api/users', - route: { path: '/:id/details' }, - originalUrl: '/api/users/123/details', - } as PolymorphicRequest, - { path: true, method: false }, - '/api/users/:id/details', - 'route' as TransactionSource, - ], - [ - 'ignores the path if specified', - { - method: 'get', - baseUrl: '/api/users', - route: { path: '/:id/details' }, - originalUrl: '/api/users/123/details', - } as PolymorphicRequest, - { path: false, method: true }, - 'GET', - 'route' as TransactionSource, - ], - [ - 'returns an empty string if everything should be ignored', - { - method: 'get', - baseUrl: '/api/users', - route: { path: '/:id/details' }, - originalUrl: '/api/users/123/details', - } as PolymorphicRequest, - { path: false, method: false }, - '', - 'route' as TransactionSource, - ], - [ - 'falls back to the raw URL if no parameterized route is available', - { - method: 'get', - baseUrl: '/api/users', - originalUrl: '/api/users/123/details', - } as PolymorphicRequest, - { path: true, method: true }, - 'GET /api/users/123/details', - 'url' as TransactionSource, - ], - ])( - '%s', - ( - _: string, - req: PolymorphicRequest, - options: { path?: boolean; method?: boolean }, - expectedRoute: string, - expectedSource: TransactionSource, - ) => { - // eslint-disable-next-line deprecation/deprecation - const [route, source] = extractPathForTransaction(req, options); - - expect(route).toEqual(expectedRoute); - expect(source).toEqual(expectedSource); - }, - ); - - it('overrides the requests information with a custom route if specified', () => { - const req = { - method: 'get', - baseUrl: '/api/users', - route: { path: '/:id/details' }, - originalUrl: '/api/users/123/details', - } as PolymorphicRequest; - - // eslint-disable-next-line deprecation/deprecation - const [route, source] = extractPathForTransaction(req, { - path: true, - method: true, - customRoute: '/other/path/:id/details', - }); - - expect(route).toEqual('GET /other/path/:id/details'); - expect(source).toEqual('route'); - }); -}); - -describe('getClientIPAddress', () => { - it.each([ - [ - '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5,2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35', - '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', - ], - [ - '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35', - '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', - ], - [ - '2a01:cb19:8350:ed00:d0dd:INVALID_IP_ADDR:8be5,141.101.69.35,2a01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', - '141.101.69.35', - ], - [ - '2b01:cb19:8350:ed00:d0dd:fa5b:nope:8be5, 2b01:cb19:NOPE:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35 ', - '141.101.69.35', - ], - ['2b01:cb19:8350:ed00:d0 dd:fa5b:de31:8be5, 141.101.69.35', '141.101.69.35'], - ])('should parse the IP from the X-Forwarded-For header %s', (headerValue, expectedIP) => { - const headers = { - 'X-Forwarded-For': headerValue, - }; - - const ip = getClientIPAddress(headers); - - expect(ip).toEqual(expectedIP); - }); -}); diff --git a/packages/core/test/utils-hoist/tracing.test.ts b/packages/core/test/utils-hoist/tracing.test.ts index 95e95c5075f9..75b39a573437 100644 --- a/packages/core/test/utils-hoist/tracing.test.ts +++ b/packages/core/test/utils-hoist/tracing.test.ts @@ -1,33 +1,33 @@ import { extractTraceparentData, propagationContextFromHeaders } from '../../src/utils-hoist/tracing'; const EXAMPLE_SENTRY_TRACE = '12312012123120121231201212312012-1121201211212012-1'; -const EXAMPLE_BAGGAGE = 'sentry-release=1.2.3,sentry-foo=bar,other=baz'; +const EXAMPLE_BAGGAGE = 'sentry-release=1.2.3,sentry-foo=bar,other=baz,sentry-sample_rand=0.42'; describe('propagationContextFromHeaders()', () => { it('returns a completely new propagation context when no sentry-trace data is given but baggage data is given', () => { const result = propagationContextFromHeaders(undefined, undefined); expect(result).toEqual({ traceId: expect.any(String), - spanId: expect.any(String), + sampleRand: expect.any(Number), }); }); it('returns a completely new propagation context when no sentry-trace data is given', () => { const result = propagationContextFromHeaders(undefined, EXAMPLE_BAGGAGE); - expect(result).toEqual({ + expect(result).toStrictEqual({ traceId: expect.any(String), - spanId: expect.any(String), + sampleRand: expect.any(Number), }); }); it('returns the correct traceparent data within the propagation context when sentry trace data is given', () => { const result = propagationContextFromHeaders(EXAMPLE_SENTRY_TRACE, undefined); - expect(result).toEqual( + expect(result).toStrictEqual( expect.objectContaining({ traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', - spanId: expect.any(String), sampled: true, + sampleRand: expect.any(Number), }), ); }); @@ -46,11 +46,12 @@ describe('propagationContextFromHeaders()', () => { expect(result).toEqual({ traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', - spanId: expect.any(String), sampled: true, + sampleRand: 0.42, dsc: { release: '1.2.3', foo: 'bar', + sample_rand: '0.42', }, }); }); @@ -81,20 +82,18 @@ describe('extractTraceparentData', () => { }); test('just sample decision - false', () => { - const data = extractTraceparentData('0') as any; + const data = extractTraceparentData('0')!; expect(data).toBeDefined(); expect(data.traceId).toBeUndefined(); - expect(data.spanId).toBeUndefined(); expect(data.parentSampled).toBeFalsy(); }); test('just sample decision - true', () => { - const data = extractTraceparentData('1') as any; + const data = extractTraceparentData('1')!; expect(data).toBeDefined(); expect(data.traceId).toBeUndefined(); - expect(data.spanId).toBeUndefined(); expect(data.parentSampled).toBeTruthy(); }); diff --git a/packages/core/test/utils-hoist/url.test.ts b/packages/core/test/utils-hoist/url.test.ts index cd793e1d3d0c..cd066201945d 100644 --- a/packages/core/test/utils-hoist/url.test.ts +++ b/packages/core/test/utils-hoist/url.test.ts @@ -1,9 +1,4 @@ -import { - getNumberOfUrlSegments, - getSanitizedUrlString, - parseUrl, - stripUrlQueryAndFragment, -} from '../../src/utils-hoist/url'; +import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '../../src/utils-hoist/url'; describe('stripQueryStringAndFragment', () => { const urlString = 'http://dogs.are.great:1231/yay/'; @@ -26,18 +21,6 @@ describe('stripQueryStringAndFragment', () => { }); }); -describe('getNumberOfUrlSegments', () => { - test.each([ - ['regular path', '/projects/123/views/234', 4], - ['single param parameterized path', '/users/:id/details', 3], - ['multi param parameterized path', '/stores/:storeId/products/:productId', 4], - ['regex path', String(/\/api\/post[0-9]/), 2], - ])('%s', (_: string, input, output) => { - // eslint-disable-next-line deprecation/deprecation - expect(getNumberOfUrlSegments(input)).toEqual(output); - }); -}); - describe('getSanitizedUrlString', () => { it.each([ ['regular url', 'https://somedomain.com', 'https://somedomain.com'], diff --git a/packages/deno/.eslintrc.js b/packages/deno/.eslintrc.js index 0c539a61b1b2..5a8ccd2be035 100644 --- a/packages/deno/.eslintrc.js +++ b/packages/deno/.eslintrc.js @@ -2,8 +2,6 @@ module.exports = { extends: ['../../.eslintrc.js'], ignorePatterns: ['lib.deno.d.ts', 'scripts/*.mjs', 'build-types/**', 'build-test/**', 'build/**'], rules: { - '@sentry-internal/sdk/no-optional-chaining': 'off', - '@sentry-internal/sdk/no-nullish-coalescing': 'off', '@sentry-internal/sdk/no-class-field-initializers': 'off', }, }; diff --git a/packages/deno/README.md b/packages/deno/README.md index 502778cf8abb..8986cdf85c8d 100644 --- a/packages/deno/README.md +++ b/packages/deno/README.md @@ -12,7 +12,6 @@ ## Links -- [SDK on Deno registry](https://deno.land/x/sentry) - [Official SDK Docs](https://docs.sentry.io/quickstart/) - [TypeDoc](http://getsentry.github.io/sentry-javascript/) @@ -25,10 +24,6 @@ To use this SDK, call `Sentry.init(options)` as early as possible in the main en 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'; - -// or import from npm registry import * as Sentry from 'npm:@sentry/deno'; Sentry.init({ diff --git a/packages/deno/lib.deno.d.ts b/packages/deno/lib.deno.d.ts index 62eec898407c..555614d581cf 100644 --- a/packages/deno/lib.deno.d.ts +++ b/packages/deno/lib.deno.d.ts @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2018-2025 the Deno authors. MIT license. /// /// @@ -8,9 +8,9 @@ * to ensure that these are still available when using the Deno namespace in * conjunction with other type libs, like `dom`. * - * @category ES Modules + * @category Platform */ -declare interface ImportMeta { +interface ImportMeta { /** A string representation of the fully qualified module URL. When the * module is loaded locally, the value will be a file URL (e.g. * `file:///path/module.ts`). @@ -28,6 +28,36 @@ declare interface ImportMeta { */ url: string; + /** The absolute path of the current module. + * + * This property is only provided for local modules (ie. using `file://` URLs). + * + * Example: + * ``` + * // Unix + * console.log(import.meta.filename); // /home/alice/my_module.ts + * + * // Windows + * console.log(import.meta.filename); // C:\alice\my_module.ts + * ``` + */ + filename?: string; + + /** The absolute path of the directory containing the current module. + * + * This property is only provided for local modules (ie. using `file://` URLs). + * + * * Example: + * ``` + * // Unix + * console.log(import.meta.dirname); // /home/alice + * + * // Windows + * console.log(import.meta.dirname); // C:\alice + * ``` + */ + dirname?: string; + /** A flag that indicates if the current module is the main module that was * called when starting the program under Deno. * @@ -59,7 +89,7 @@ declare interface ImportMeta { * * @category Performance */ -declare interface Performance { +interface Performance { /** Stores a timestamp with the associated name (a "mark"). */ mark(markName: string, options?: PerformanceMarkOptions): PerformanceMark; @@ -79,7 +109,7 @@ declare interface Performance { * * @category Performance */ -declare interface PerformanceMarkOptions { +interface PerformanceMarkOptions { /** Metadata to be included in the mark. */ // deno-lint-ignore no-explicit-any detail?: any; @@ -96,7 +126,7 @@ declare interface PerformanceMarkOptions { * * @category Performance */ -declare interface PerformanceMeasureOptions { +interface PerformanceMeasureOptions { /** Metadata to be included in the measure. */ // deno-lint-ignore no-explicit-any detail?: any; @@ -145,8 +175,11 @@ declare namespace Deno { /** * Raised when the underlying operating system indicates the current user * which the Deno process is running under does not have the appropriate - * permissions to a file or resource, or the user _did not_ provide required - * `--allow-*` flag. + * permissions to a file or resource. + * + * Before Deno 2.0, this error was raised when the user _did not_ provide + * required `--allow-*` flag. As of Deno 2.0, that case is now handled by + * the {@link NotCapable} error. * * @category Errors */ export class PermissionDenied extends Error {} @@ -284,6 +317,18 @@ declare namespace Deno { * * @category Errors */ export class NotADirectory extends Error {} + + /** + * Raised when trying to perform an operation while the relevant Deno + * permission (like `--allow-read`) has not been granted. + * + * Before Deno 2.0, this condition was covered by the {@link PermissionDenied} + * error. + * + * @category Errors */ + export class NotCapable extends Error {} + + export {}; // only export exports } /** The current process ID of this instance of the Deno CLI. @@ -292,7 +337,7 @@ declare namespace Deno { * console.log(Deno.pid); * ``` * - * @category Runtime Environment + * @category Runtime */ export const pid: number; @@ -303,11 +348,11 @@ declare namespace Deno { * console.log(Deno.ppid); * ``` * - * @category Runtime Environment + * @category Runtime */ export const ppid: number; - /** @category Runtime Environment */ + /** @category Runtime */ export interface MemoryUsage { /** The number of bytes of the current Deno's process resident set size, * which is the amount of memory occupied in main memory (RAM). */ @@ -325,7 +370,7 @@ declare namespace Deno { * Returns an object describing the memory usage of the Deno process and the * V8 subsystem measured in bytes. * - * @category Runtime Environment + * @category Runtime */ export function memoryUsage(): MemoryUsage; @@ -339,7 +384,7 @@ declare namespace Deno { * Requires `allow-sys` permission. * * @tags allow-sys - * @category Runtime Environment + * @category Runtime */ export function hostname(): string; @@ -359,7 +404,7 @@ declare namespace Deno { * On Windows there is no API available to retrieve this information and this method returns `[ 0, 0, 0 ]`. * * @tags allow-sys - * @category Observability + * @category Runtime */ export function loadavg(): number[]; @@ -413,14 +458,14 @@ declare namespace Deno { * Requires `allow-sys` permission. * * @tags allow-sys - * @category Runtime Environment + * @category Runtime */ export function systemMemoryInfo(): SystemMemoryInfo; /** * Information returned from a call to {@linkcode Deno.systemMemoryInfo}. * - * @category Runtime Environment + * @category Runtime */ export interface SystemMemoryInfo { /** Total installed memory in bytes. */ @@ -451,7 +496,7 @@ declare namespace Deno { * * See: https://no-color.org/ * - * @category Runtime Environment + * @category Runtime */ export const noColor: boolean; @@ -467,7 +512,7 @@ declare namespace Deno { * it should depend sys-info, which may not be desirable. * * @tags allow-sys - * @category Runtime Environment + * @category Runtime */ export function osRelease(): string; @@ -481,7 +526,7 @@ declare namespace Deno { * Requires `allow-sys` permission. * * @tags allow-sys - * @category Runtime Environment + * @category Runtime */ export function osUptime(): number; @@ -494,10 +539,7 @@ declare namespace Deno { * set of permissions to the test context. * * @category Permissions */ - export type PermissionOptions = - | "inherit" - | "none" - | PermissionOptionsObject; + export type PermissionOptions = "inherit" | "none" | PermissionOptionsObject; /** * A set of options which can define the permissions within a test or worker @@ -514,23 +556,23 @@ declare namespace Deno { */ env?: "inherit" | boolean | string[]; - /** Specifies if the `sys` permission should be requested or revoked. - * If set to `"inherit"`, the current `sys` permission will be inherited. - * If set to `true`, the global `sys` permission will be requested. - * If set to `false`, the global `sys` permission will be revoked. + /** Specifies if the `ffi` permission should be requested or revoked. + * If set to `"inherit"`, the current `ffi` permission will be inherited. + * If set to `true`, the global `ffi` permission will be requested. + * If set to `false`, the global `ffi` permission will be revoked. * * @default {false} */ - sys?: "inherit" | boolean | string[]; + ffi?: "inherit" | boolean | Array; - /** Specifies if the `hrtime` permission should be requested or revoked. - * If set to `"inherit"`, the current `hrtime` permission will be inherited. - * If set to `true`, the global `hrtime` permission will be requested. - * If set to `false`, the global `hrtime` permission will be revoked. - * - * @default {false} + /** Specifies if the `import` permission should be requested or revoked. + * If set to `"inherit"` the current `import` permission will be inherited. + * If set to `true`, the global `import` permission will be requested. + * If set to `false`, the global `import` permission will be revoked. + * If set to `Array`, the `import` permissions will be requested with the + * specified domains. */ - hrtime?: "inherit" | boolean; + import?: "inherit" | boolean | Array; /** Specifies if the `net` permission should be requested or revoked. * if set to `"inherit"`, the current `net` permission will be inherited. @@ -544,7 +586,7 @@ declare namespace Deno { * Examples: * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.test({ * name: "inherit", @@ -559,7 +601,7 @@ declare namespace Deno { * ``` * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.test({ * name: "true", @@ -574,7 +616,7 @@ declare namespace Deno { * ``` * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.test({ * name: "false", @@ -589,7 +631,7 @@ declare namespace Deno { * ``` * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.test({ * name: "localhost:8080", @@ -605,15 +647,6 @@ declare namespace Deno { */ net?: "inherit" | boolean | string[]; - /** Specifies if the `ffi` permission should be requested or revoked. - * If set to `"inherit"`, the current `ffi` permission will be inherited. - * If set to `true`, the global `ffi` permission will be requested. - * If set to `false`, the global `ffi` permission will be revoked. - * - * @default {false} - */ - ffi?: "inherit" | boolean | Array; - /** Specifies if the `read` permission should be requested or revoked. * If set to `"inherit"`, the current `read` permission will be inherited. * If set to `true`, the global `read` permission will be requested. @@ -634,6 +667,15 @@ declare namespace Deno { */ run?: "inherit" | boolean | Array; + /** Specifies if the `sys` permission should be requested or revoked. + * If set to `"inherit"`, the current `sys` permission will be inherited. + * If set to `true`, the global `sys` permission will be requested. + * If set to `false`, the global `sys` permission will be revoked. + * + * @default {false} + */ + sys?: "inherit" | boolean | string[]; + /** Specifies if the `write` permission should be requested or revoked. * If set to `"inherit"`, the current `write` permission will be inherited. * If set to `true`, the global `write` permission will be requested. @@ -818,7 +860,7 @@ declare namespace Deno { * `fn` can be async if required. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.test({ * name: "example test", @@ -852,14 +894,14 @@ declare namespace Deno { /** * @category Testing */ - interface DenoTest { + export interface DenoTest { /** Register a test which will be run when `deno test` is used on the command * line and the containing module looks like a test module. * * `fn` can be async if required. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.test({ * name: "example test", @@ -896,7 +938,7 @@ declare namespace Deno { * `fn` can be async if required. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.test("My test description", () => { * assertEquals("hello", "hello"); @@ -911,10 +953,7 @@ declare namespace Deno { * * @category Testing */ - ( - name: string, - fn: (t: TestContext) => void | Promise, - ): void; + (name: string, fn: (t: TestContext) => void | Promise): void; /** Register a test which will be run when `deno test` is used on the command * line and the containing module looks like a test module. @@ -922,7 +961,7 @@ declare namespace Deno { * `fn` can be async if required. Declared function must have a name. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.test(function myTestName() { * assertEquals("hello", "hello"); @@ -945,7 +984,7 @@ declare namespace Deno { * `fn` can be async if required. * * ```ts - * import {assert, fail, assertEquals} from "https://deno.land/std/assert/mod.ts"; + * import { assert, fail, assertEquals } from "jsr:@std/assert"; * * Deno.test("My test description", { permissions: { read: true } }, (): void => { * assertEquals("hello", "hello"); @@ -972,7 +1011,7 @@ declare namespace Deno { * `fn` can be async if required. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.test( * { @@ -1010,7 +1049,7 @@ declare namespace Deno { * `fn` can be async if required. Declared function must have a name. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.test( * { permissions: { read: true } }, @@ -1046,10 +1085,7 @@ declare namespace Deno { * * @category Testing */ - ignore( - name: string, - fn: (t: TestContext) => void | Promise, - ): void; + ignore(name: string, fn: (t: TestContext) => void | Promise): void; /** Shorthand property for ignoring a particular test case. * @@ -1095,10 +1131,7 @@ declare namespace Deno { * * @category Testing */ - only( - name: string, - fn: (t: TestContext) => void | Promise, - ): void; + only(name: string, fn: (t: TestContext) => void | Promise): void; /** Shorthand property for focusing a particular test case. * @@ -1174,11 +1207,10 @@ declare namespace Deno { * * ```ts * Deno.bench("foo", async (t) => { - * const file = await Deno.open("data.txt"); + * using file = await Deno.open("data.txt"); * t.start(); * // some operation on `file`... * t.end(); - * file.close(); * }); * ``` */ @@ -1234,7 +1266,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.bench({ * name: "example test", @@ -1273,7 +1305,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.bench("My test description", () => { * assertEquals("hello", "hello"); @@ -1301,7 +1333,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.bench(function myTestName() { * assertEquals("hello", "hello"); @@ -1326,7 +1358,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.bench( * "My test description", @@ -1363,7 +1395,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.bench( * { name: "My test description", permissions: { read: true } }, @@ -1397,7 +1429,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; + * import { assertEquals } from "jsr:@std/assert"; * * Deno.bench( * { permissions: { read: true } }, @@ -1433,15 +1465,32 @@ declare namespace Deno { * Deno.exit(5); * ``` * - * @category Runtime Environment + * @category Runtime */ export function exit(code?: number): never; + /** The exit code for the Deno process. + * + * If no exit code has been supplied, then Deno will assume a return code of `0`. + * + * When setting an exit code value, a number or non-NaN string must be provided, + * otherwise a TypeError will be thrown. + * + * ```ts + * console.log(Deno.exitCode); //-> 0 + * Deno.exitCode = 1; + * console.log(Deno.exitCode); //-> 1 + * ``` + * + * @category Runtime + */ + export var exitCode: number; + /** An interface containing methods to interact with the process environment * variables. * * @tags allow-env - * @category Runtime Environment + * @category Runtime */ export interface Env { /** Retrieve the value of an environment variable. @@ -1520,7 +1569,7 @@ declare namespace Deno { * variables. * * @tags allow-env - * @category Runtime Environment + * @category Runtime */ export const env: Env; @@ -1534,7 +1583,7 @@ declare namespace Deno { * Requires `allow-read` permission. * * @tags allow-read - * @category Runtime Environment + * @category Runtime */ export function execPath(): string; @@ -1555,7 +1604,7 @@ declare namespace Deno { * Requires `allow-read` permission. * * @tags allow-read - * @category Runtime Environment + * @category Runtime */ export function chdir(directory: string | URL): void; @@ -1574,7 +1623,7 @@ declare namespace Deno { * Requires `allow-read` permission. * * @tags allow-read - * @category Runtime Environment + * @category Runtime */ export function cwd(): string; @@ -1620,234 +1669,19 @@ declare namespace Deno { End = 2, } - /** - * An abstract interface which when implemented provides an interface to read - * bytes into an array buffer asynchronously. - * - * @deprecated Use {@linkcode ReadableStream} instead. {@linkcode Reader} - * will be removed in v2.0.0. - * - * @category I/O */ - export interface Reader { - /** Reads up to `p.byteLength` bytes into `p`. It resolves to the number of - * bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error - * encountered. Even if `read()` resolves to `n` < `p.byteLength`, it may - * use all of `p` as scratch space during the call. If some data is - * available but not `p.byteLength` bytes, `read()` conventionally resolves - * to what is available instead of waiting for more. - * - * When `read()` encounters end-of-file condition, it resolves to EOF - * (`null`). - * - * When `read()` encounters an error, it rejects with an error. - * - * Callers should always process the `n` > `0` bytes returned before - * considering the EOF (`null`). Doing so correctly handles I/O errors that - * happen after reading some bytes and also both of the allowed EOF - * behaviors. - * - * Implementations should not retain a reference to `p`. - * - * Use - * [`itereateReader`](https://deno.land/std/streams/iterate_reader.ts?s=iterateReader) - * from - * [`std/streams/iterate_reader.ts`](https://deno.land/std/streams/iterate_reader.ts) - * to turn a `Reader` into an {@linkcode AsyncIterator}. - */ - read(p: Uint8Array): Promise; - } - - /** - * An abstract interface which when implemented provides an interface to read - * bytes into an array buffer synchronously. - * - * @deprecated Use {@linkcode ReadableStream} instead. {@linkcode ReaderSync} - * will be removed in v2.0.0. - * - * @category I/O */ - export interface ReaderSync { - /** Reads up to `p.byteLength` bytes into `p`. It resolves to the number - * of bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error - * encountered. Even if `readSync()` returns `n` < `p.byteLength`, it may use - * all of `p` as scratch space during the call. If some data is available - * but not `p.byteLength` bytes, `readSync()` conventionally returns what is - * available instead of waiting for more. - * - * When `readSync()` encounters end-of-file condition, it returns EOF - * (`null`). - * - * When `readSync()` encounters an error, it throws with an error. - * - * Callers should always process the `n` > `0` bytes returned before - * considering the EOF (`null`). Doing so correctly handles I/O errors that - * happen after reading some bytes and also both of the allowed EOF - * behaviors. - * - * Implementations should not retain a reference to `p`. - * - * Use - * [`itereateReaderSync`](https://deno.land/std/streams/iterate_reader.ts?s=iterateReaderSync) - * from from - * [`std/streams/iterate_reader.ts`](https://deno.land/std/streams/iterate_reader.ts) - * to turn a `ReaderSync` into an {@linkcode Iterator}. - */ - readSync(p: Uint8Array): number | null; - } - - /** - * An abstract interface which when implemented provides an interface to write - * bytes from an array buffer to a file/resource asynchronously. - * - * @deprecated Use {@linkcode WritableStream} instead. {@linkcode Writer} - * will be removed in v2.0.0. - * - * @category I/O */ - export interface Writer { - /** Writes `p.byteLength` bytes from `p` to the underlying data stream. It - * resolves to the number of bytes written from `p` (`0` <= `n` <= - * `p.byteLength`) or reject with the error encountered that caused the - * write to stop early. `write()` must reject with a non-null error if - * would resolve to `n` < `p.byteLength`. `write()` must not modify the - * slice data, even temporarily. - * - * This function is one of the lowest - * level APIs and most users should not work with this directly, but rather use - * [`writeAll()`](https://deno.land/std/streams/write_all.ts?s=writeAll) from - * [`std/streams/write_all.ts`](https://deno.land/std/streams/write_all.ts) - * instead. - * - * Implementations should not retain a reference to `p`. - */ - write(p: Uint8Array): Promise; - } - - /** - * An abstract interface which when implemented provides an interface to write - * bytes from an array buffer to a file/resource synchronously. - * - * @deprecated Use {@linkcode WritableStream} instead. {@linkcode WriterSync} - * will be removed in v2.0.0. - * - * @category I/O */ - export interface WriterSync { - /** Writes `p.byteLength` bytes from `p` to the underlying data - * stream. It returns the number of bytes written from `p` (`0` <= `n` - * <= `p.byteLength`) and any error encountered that caused the write to - * stop early. `writeSync()` must throw a non-null error if it returns `n` < - * `p.byteLength`. `writeSync()` must not modify the slice data, even - * temporarily. - * - * Implementations should not retain a reference to `p`. - */ - writeSync(p: Uint8Array): number; - } - - /** - * An abstract interface which when implemented provides an interface to close - * files/resources that were previously opened. - * - * @deprecated Use {@linkcode ReadableStream} and {@linkcode WritableStream} - * instead. {@linkcode Closer} will be removed in v2.0.0. - * - * @category I/O */ - export interface Closer { - /** Closes the resource, "freeing" the backing file/resource. */ - close(): void; - } - - /** - * An abstract interface which when implemented provides an interface to seek - * within an open file/resource asynchronously. - * - * @category I/O */ - export interface Seeker { - /** Seek sets the offset for the next `read()` or `write()` to offset, - * interpreted according to `whence`: `Start` means relative to the - * start of the file, `Current` means relative to the current offset, - * and `End` means relative to the end. Seek resolves to the new offset - * relative to the start of the file. - * - * Seeking to an offset before the start of the file is an error. Seeking to - * any positive offset is legal, but the behavior of subsequent I/O - * operations on the underlying object is implementation-dependent. - * - * It resolves with the updated offset. - */ - seek(offset: number | bigint, whence: SeekMode): Promise; - } - - /** - * An abstract interface which when implemented provides an interface to seek - * within an open file/resource synchronously. - * - * @category I/O */ - export interface SeekerSync { - /** Seek sets the offset for the next `readSync()` or `writeSync()` to - * offset, interpreted according to `whence`: `Start` means relative - * to the start of the file, `Current` means relative to the current - * offset, and `End` means relative to the end. - * - * Seeking to an offset before the start of the file is an error. Seeking to - * any positive offset is legal, but the behavior of subsequent I/O - * operations on the underlying object is implementation-dependent. - * - * It returns the updated offset. - */ - seekSync(offset: number | bigint, whence: SeekMode): number; - } - - /** - * Copies from `src` to `dst` until either EOF (`null`) is read from `src` or - * an error occurs. It resolves to the number of bytes copied or rejects with - * the first error encountered while copying. - * - * @deprecated Use {@linkcode ReadableStream.pipeTo} instead. - * {@linkcode Deno.copy} will be removed in the future. - * - * @category I/O - * - * @param src The source to copy from - * @param dst The destination to copy to - * @param options Can be used to tune size of the buffer. Default size is 32kB - */ - export function copy( - src: Reader, - dst: Writer, - options?: { bufSize?: number }, - ): Promise; - - /** - * Turns a Reader, `r`, into an async iterator. - * - * @deprecated Use {@linkcode ReadableStream} instead. {@linkcode Deno.iter} - * will be removed in the future. - * - * @category I/O - */ - export function iter( - r: Reader, - options?: { bufSize?: number }, - ): AsyncIterableIterator; - - /** - * Turns a ReaderSync, `r`, into an iterator. - * - * @deprecated Use {@linkcode ReadableStream} instead. - * {@linkcode Deno.iterSync} will be removed in the future. - * - * @category I/O - */ - export function iterSync( - r: ReaderSync, - options?: { - bufSize?: number; - }, - ): IterableIterator; - /** Open a file and resolve to an instance of {@linkcode Deno.FsFile}. The * file does not need to previously exist if using the `create` or `createNew` - * open options. It is the caller's responsibility to close the file when - * finished with it. + * open options. The caller may have the resulting file automatically closed + * by the runtime once it's out of scope by declaring the file variable with + * the `using` keyword. + * + * ```ts + * using file = await Deno.open("/foo/bar.txt", { read: true, write: true }); + * // Do work with file + * ``` + * + * Alternatively, the caller may manually close the resource when finished with + * it. * * ```ts * const file = await Deno.open("/foo/bar.txt", { read: true, write: true }); @@ -1868,8 +1702,17 @@ declare namespace Deno { /** Synchronously open a file and return an instance of * {@linkcode Deno.FsFile}. The file does not need to previously exist if - * using the `create` or `createNew` open options. It is the caller's - * responsibility to close the file when finished with it. + * using the `create` or `createNew` open options. The caller may have the + * resulting file automatically closed by the runtime once it's out of scope + * by declaring the file variable with the `using` keyword. + * + * ```ts + * using file = Deno.openSync("/foo/bar.txt", { read: true, write: true }); + * // Do work with file + * ``` + * + * Alternatively, the caller may manually close the resource when finished with + * it. * * ```ts * const file = Deno.openSync("/foo/bar.txt", { read: true, write: true }); @@ -1913,326 +1756,30 @@ declare namespace Deno { */ export function createSync(path: string | URL): FsFile; - /** Read from a resource ID (`rid`) into an array buffer (`buffer`). - * - * Resolves to either the number of bytes read during the operation or EOF - * (`null`) if there was nothing more to read. - * - * It is possible for a read to successfully return with `0` bytes. This does - * not indicate EOF. - * - * This function is one of the lowest level APIs and most users should not - * work with this directly, but rather use {@linkcode ReadableStream} and - * {@linkcode https://deno.land/std/streams/mod.ts?s=toArrayBuffer|toArrayBuffer} - * instead. - * - * **It is not guaranteed that the full buffer will be read in a single call.** - * - * ```ts - * // if "/foo/bar.txt" contains the text "hello world": - * const file = await Deno.open("/foo/bar.txt"); - * const buf = new Uint8Array(100); - * const numberOfBytesRead = await Deno.read(file.rid, buf); // 11 bytes - * const text = new TextDecoder().decode(buf); // "hello world" - * Deno.close(file.rid); - * ``` - * - * @category I/O - */ - export function read(rid: number, buffer: Uint8Array): Promise; - - /** Synchronously read from a resource ID (`rid`) into an array buffer - * (`buffer`). - * - * Returns either the number of bytes read during the operation or EOF - * (`null`) if there was nothing more to read. - * - * It is possible for a read to successfully return with `0` bytes. This does - * not indicate EOF. - * - * This function is one of the lowest level APIs and most users should not - * work with this directly, but rather use {@linkcode ReadableStream} and - * {@linkcode https://deno.land/std/streams/mod.ts?s=toArrayBuffer|toArrayBuffer} - * instead. - * - * **It is not guaranteed that the full buffer will be read in a single - * call.** - * - * ```ts - * // if "/foo/bar.txt" contains the text "hello world": - * const file = Deno.openSync("/foo/bar.txt"); - * const buf = new Uint8Array(100); - * const numberOfBytesRead = Deno.readSync(file.rid, buf); // 11 bytes - * const text = new TextDecoder().decode(buf); // "hello world" - * Deno.close(file.rid); - * ``` - * - * @category I/O - */ - export function readSync(rid: number, buffer: Uint8Array): number | null; - - /** Write to the resource ID (`rid`) the contents of the array buffer (`data`). - * - * Resolves to the number of bytes written. This function is one of the lowest - * level APIs and most users should not work with this directly, but rather - * use {@linkcode WritableStream}, {@linkcode ReadableStream.from} and - * {@linkcode ReadableStream.pipeTo}. - * - * **It is not guaranteed that the full buffer will be written in a single - * call.** - * - * ```ts - * const encoder = new TextEncoder(); - * const data = encoder.encode("Hello world"); - * const file = await Deno.open("/foo/bar.txt", { write: true }); - * const bytesWritten = await Deno.write(file.rid, data); // 11 - * Deno.close(file.rid); - * ``` - * - * @category I/O - */ - export function write(rid: number, data: Uint8Array): Promise; - - /** Synchronously write to the resource ID (`rid`) the contents of the array - * buffer (`data`). - * - * Returns the number of bytes written. This function is one of the lowest - * level APIs and most users should not work with this directly, but rather - * use {@linkcode WritableStream}, {@linkcode ReadableStream.from} and - * {@linkcode ReadableStream.pipeTo}. - * - * **It is not guaranteed that the full buffer will be written in a single - * call.** - * - * ```ts - * const encoder = new TextEncoder(); - * const data = encoder.encode("Hello world"); - * const file = Deno.openSync("/foo/bar.txt", { write: true }); - * const bytesWritten = Deno.writeSync(file.rid, data); // 11 - * Deno.close(file.rid); - * ``` - * - * @category I/O - */ - export function writeSync(rid: number, data: Uint8Array): number; - - /** Seek a resource ID (`rid`) to the given `offset` under mode given by `whence`. - * The call resolves to the new position within the resource (bytes from the start). - * - * ```ts - * // Given file.rid pointing to file with "Hello world", which is 11 bytes long: - * const file = await Deno.open( - * "hello.txt", - * { read: true, write: true, truncate: true, create: true }, - * ); - * await Deno.write(file.rid, new TextEncoder().encode("Hello world")); - * - * // advance cursor 6 bytes - * const cursorPosition = await Deno.seek(file.rid, 6, Deno.SeekMode.Start); - * console.log(cursorPosition); // 6 - * const buf = new Uint8Array(100); - * await file.read(buf); - * console.log(new TextDecoder().decode(buf)); // "world" - * file.close(); - * ``` - * - * The seek modes work as follows: - * - * ```ts - * // Given file.rid pointing to file with "Hello world", which is 11 bytes long: - * const file = await Deno.open( - * "hello.txt", - * { read: true, write: true, truncate: true, create: true }, - * ); - * await Deno.write(file.rid, new TextEncoder().encode("Hello world")); - * - * // Seek 6 bytes from the start of the file - * console.log(await Deno.seek(file.rid, 6, Deno.SeekMode.Start)); // "6" - * // Seek 2 more bytes from the current position - * console.log(await Deno.seek(file.rid, 2, Deno.SeekMode.Current)); // "8" - * // Seek backwards 2 bytes from the end of the file - * console.log(await Deno.seek(file.rid, -2, Deno.SeekMode.End)); // "9" (i.e. 11-2) - * file.close(); - * ``` - * - * @category I/O - */ - export function seek( - rid: number, - offset: number | bigint, - whence: SeekMode, - ): Promise; - - /** Synchronously seek a resource ID (`rid`) to the given `offset` under mode - * given by `whence`. The new position within the resource (bytes from the - * start) is returned. - * - * ```ts - * const file = Deno.openSync( - * "hello.txt", - * { read: true, write: true, truncate: true, create: true }, - * ); - * Deno.writeSync(file.rid, new TextEncoder().encode("Hello world")); - * - * // advance cursor 6 bytes - * const cursorPosition = Deno.seekSync(file.rid, 6, Deno.SeekMode.Start); - * console.log(cursorPosition); // 6 - * const buf = new Uint8Array(100); - * file.readSync(buf); - * console.log(new TextDecoder().decode(buf)); // "world" - * file.close(); - * ``` + /** The Deno abstraction for reading and writing files. * - * The seek modes work as follows: + * This is the most straight forward way of handling files within Deno and is + * recommended over using the discrete functions within the `Deno` namespace. * * ```ts - * // Given file.rid pointing to file with "Hello world", which is 11 bytes long: - * const file = Deno.openSync( - * "hello.txt", - * { read: true, write: true, truncate: true, create: true }, - * ); - * Deno.writeSync(file.rid, new TextEncoder().encode("Hello world")); - * - * // Seek 6 bytes from the start of the file - * console.log(Deno.seekSync(file.rid, 6, Deno.SeekMode.Start)); // "6" - * // Seek 2 more bytes from the current position - * console.log(Deno.seekSync(file.rid, 2, Deno.SeekMode.Current)); // "8" - * // Seek backwards 2 bytes from the end of the file - * console.log(Deno.seekSync(file.rid, -2, Deno.SeekMode.End)); // "9" (i.e. 11-2) - * file.close(); + * using file = await Deno.open("/foo/bar.txt", { read: true }); + * const fileInfo = await file.stat(); + * if (fileInfo.isFile) { + * const buf = new Uint8Array(100); + * const numberOfBytesRead = await file.read(buf); // 11 bytes + * const text = new TextDecoder().decode(buf); // "hello world" + * } * ``` * - * @category I/O + * @category File System */ - export function seekSync( - rid: number, - offset: number | bigint, - whence: SeekMode, - ): number; - - /** - * Flushes any pending data and metadata operations of the given file stream - * to disk. - * - * ```ts - * const file = await Deno.open( - * "my_file.txt", - * { read: true, write: true, create: true }, - * ); - * await Deno.write(file.rid, new TextEncoder().encode("Hello World")); - * await Deno.ftruncate(file.rid, 1); - * await Deno.fsync(file.rid); - * console.log(new TextDecoder().decode(await Deno.readFile("my_file.txt"))); // H - * ``` - * - * @category I/O - */ - export function fsync(rid: number): Promise; - - /** - * Synchronously flushes any pending data and metadata operations of the given - * file stream to disk. - * - * ```ts - * const file = Deno.openSync( - * "my_file.txt", - * { read: true, write: true, create: true }, - * ); - * Deno.writeSync(file.rid, new TextEncoder().encode("Hello World")); - * Deno.ftruncateSync(file.rid, 1); - * Deno.fsyncSync(file.rid); - * console.log(new TextDecoder().decode(Deno.readFileSync("my_file.txt"))); // H - * ``` - * - * @category I/O - */ - export function fsyncSync(rid: number): void; - - /** - * Flushes any pending data operations of the given file stream to disk. - * ```ts - * const file = await Deno.open( - * "my_file.txt", - * { read: true, write: true, create: true }, - * ); - * await Deno.write(file.rid, new TextEncoder().encode("Hello World")); - * await Deno.fdatasync(file.rid); - * console.log(new TextDecoder().decode(await Deno.readFile("my_file.txt"))); // Hello World - * ``` - * - * @category I/O - */ - export function fdatasync(rid: number): Promise; - - /** - * Synchronously flushes any pending data operations of the given file stream - * to disk. - * - * ```ts - * const file = Deno.openSync( - * "my_file.txt", - * { read: true, write: true, create: true }, - * ); - * Deno.writeSync(file.rid, new TextEncoder().encode("Hello World")); - * Deno.fdatasyncSync(file.rid); - * console.log(new TextDecoder().decode(Deno.readFileSync("my_file.txt"))); // Hello World - * ``` - * - * @category I/O - */ - export function fdatasyncSync(rid: number): void; - - /** Close the given resource ID (`rid`) which has been previously opened, such - * as via opening or creating a file. Closing a file when you are finished - * with it is important to avoid leaking resources. - * - * ```ts - * const file = await Deno.open("my_file.txt"); - * // do work with "file" object - * Deno.close(file.rid); - * ``` - * - * @category I/O - */ - export function close(rid: number): void; - - /** The Deno abstraction for reading and writing files. - * - * This is the most straight forward way of handling files within Deno and is - * recommended over using the discreet functions within the `Deno` namespace. - * - * ```ts - * const file = await Deno.open("/foo/bar.txt", { read: true }); - * const fileInfo = await file.stat(); - * if (fileInfo.isFile) { - * const buf = new Uint8Array(100); - * const numberOfBytesRead = await file.read(buf); // 11 bytes - * const text = new TextDecoder().decode(buf); // "hello world" - * } - * file.close(); - * ``` - * - * @category File System - */ - export class FsFile - implements - Reader, - ReaderSync, - Writer, - WriterSync, - Seeker, - SeekerSync, - Closer, - Disposable { - /** The resource ID associated with the file instance. The resource ID - * should be considered an opaque reference to resource. */ - readonly rid: number; + export class FsFile implements Disposable { /** A {@linkcode ReadableStream} instance representing to the byte contents * of the file. This makes it easy to interoperate with other web streams * based APIs. * * ```ts - * const file = await Deno.open("my_file.txt", { read: true }); + * using file = await Deno.open("my_file.txt", { read: true }); * const decoder = new TextDecoder(); * for await (const chunk of file.readable) { * console.log(decoder.decode(chunk)); @@ -2246,20 +1793,15 @@ declare namespace Deno { * * ```ts * const items = ["hello", "world"]; - * const file = await Deno.open("my_file.txt", { write: true }); + * using file = await Deno.open("my_file.txt", { write: true }); * const encoder = new TextEncoder(); * const writer = file.writable.getWriter(); * for (const item of items) { * await writer.write(encoder.encode(item)); * } - * file.close(); * ``` */ readonly writable: WritableStream; - /** The constructor which takes a resource ID. Generally `FsFile` should - * not be constructed directly. Instead use {@linkcode Deno.open} or - * {@linkcode Deno.openSync} to create a new instance of `FsFile`. */ - constructor(rid: number); /** Write the contents of the array buffer (`p`) to the file. * * Resolves to the number of bytes written. @@ -2270,9 +1812,8 @@ declare namespace Deno { * ```ts * const encoder = new TextEncoder(); * const data = encoder.encode("Hello world"); - * const file = await Deno.open("/foo/bar.txt", { write: true }); + * using file = await Deno.open("/foo/bar.txt", { write: true }); * const bytesWritten = await file.write(data); // 11 - * file.close(); * ``` * * @category I/O @@ -2288,9 +1829,8 @@ declare namespace Deno { * ```ts * const encoder = new TextEncoder(); * const data = encoder.encode("Hello world"); - * const file = Deno.openSync("/foo/bar.txt", { write: true }); + * using file = Deno.openSync("/foo/bar.txt", { write: true }); * const bytesWritten = file.writeSync(data); // 11 - * file.close(); * ``` */ writeSync(p: Uint8Array): number; @@ -2300,21 +1840,19 @@ declare namespace Deno { * ### Truncate the entire file * * ```ts - * const file = await Deno.open("my_file.txt", { write: true }); + * using file = await Deno.open("my_file.txt", { write: true }); * await file.truncate(); - * file.close(); * ``` * * ### Truncate part of the file * * ```ts * // if "my_file.txt" contains the text "hello world": - * const file = await Deno.open("my_file.txt", { write: true }); + * using file = await Deno.open("my_file.txt", { write: true }); * await file.truncate(7); * const buf = new Uint8Array(100); * await file.read(buf); * const text = new TextDecoder().decode(buf); // "hello w" - * file.close(); * ``` */ truncate(len?: number): Promise; @@ -2325,21 +1863,19 @@ declare namespace Deno { * ### Truncate the entire file * * ```ts - * const file = Deno.openSync("my_file.txt", { write: true }); + * using file = Deno.openSync("my_file.txt", { write: true }); * file.truncateSync(); - * file.close(); * ``` * * ### Truncate part of the file * * ```ts * // if "my_file.txt" contains the text "hello world": - * const file = Deno.openSync("my_file.txt", { write: true }); + * using file = Deno.openSync("my_file.txt", { write: true }); * file.truncateSync(7); * const buf = new Uint8Array(100); * file.readSync(buf); * const text = new TextDecoder().decode(buf); // "hello w" - * file.close(); * ``` */ truncateSync(len?: number): void; @@ -2356,11 +1892,10 @@ declare namespace Deno { * * ```ts * // if "/foo/bar.txt" contains the text "hello world": - * const file = await Deno.open("/foo/bar.txt"); + * using file = await Deno.open("/foo/bar.txt"); * const buf = new Uint8Array(100); * const numberOfBytesRead = await file.read(buf); // 11 bytes * const text = new TextDecoder().decode(buf); // "hello world" - * file.close(); * ``` */ read(p: Uint8Array): Promise; @@ -2377,11 +1912,10 @@ declare namespace Deno { * * ```ts * // if "/foo/bar.txt" contains the text "hello world": - * const file = Deno.openSync("/foo/bar.txt"); + * using file = Deno.openSync("/foo/bar.txt"); * const buf = new Uint8Array(100); * const numberOfBytesRead = file.readSync(buf); // 11 bytes * const text = new TextDecoder().decode(buf); // "hello world" - * file.close(); * ``` */ readSync(p: Uint8Array): number | null; @@ -2389,8 +1923,8 @@ declare namespace Deno { * resolves to the new position within the resource (bytes from the start). * * ```ts - * // Given file pointing to file with "Hello world", which is 11 bytes long: - * const file = await Deno.open( + * // Given the file contains "Hello world" text, which is 11 bytes long: + * using file = await Deno.open( * "hello.txt", * { read: true, write: true, truncate: true, create: true }, * ); @@ -2402,13 +1936,12 @@ declare namespace Deno { * const buf = new Uint8Array(100); * await file.read(buf); * console.log(new TextDecoder().decode(buf)); // "world" - * file.close(); * ``` * * The seek modes work as follows: * * ```ts - * // Given file.rid pointing to file with "Hello world", which is 11 bytes long: + * // Given the file contains "Hello world" text, which is 11 bytes long: * const file = await Deno.open( * "hello.txt", * { read: true, write: true, truncate: true, create: true }, @@ -2428,7 +1961,7 @@ declare namespace Deno { * The new position within the resource (bytes from the start) is returned. * * ```ts - * const file = Deno.openSync( + * using file = Deno.openSync( * "hello.txt", * { read: true, write: true, truncate: true, create: true }, * ); @@ -2440,14 +1973,13 @@ declare namespace Deno { * const buf = new Uint8Array(100); * file.readSync(buf); * console.log(new TextDecoder().decode(buf)); // "world" - * file.close(); * ``` * * The seek modes work as follows: * * ```ts - * // Given file.rid pointing to file with "Hello world", which is 11 bytes long: - * const file = Deno.openSync( + * // Given the file contains "Hello world" text, which is 11 bytes long: + * using file = Deno.openSync( * "hello.txt", * { read: true, write: true, truncate: true, create: true }, * ); @@ -2459,41 +1991,176 @@ declare namespace Deno { * console.log(file.seekSync(2, Deno.SeekMode.Current)); // "8" * // Seek backwards 2 bytes from the end of the file * console.log(file.seekSync(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2) - * file.close(); * ``` */ seekSync(offset: number | bigint, whence: SeekMode): number; /** Resolves to a {@linkcode Deno.FileInfo} for the file. * * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; + * import { assert } from "jsr:@std/assert"; * - * const file = await Deno.open("hello.txt"); + * using file = await Deno.open("hello.txt"); * const fileInfo = await file.stat(); * assert(fileInfo.isFile); - * file.close(); * ``` */ stat(): Promise; /** Synchronously returns a {@linkcode Deno.FileInfo} for the file. * * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; + * import { assert } from "jsr:@std/assert"; * - * const file = Deno.openSync("hello.txt") + * using file = Deno.openSync("hello.txt") * const fileInfo = file.statSync(); * assert(fileInfo.isFile); - * file.close(); * ``` */ statSync(): FileInfo; + /** + * Flushes any pending data and metadata operations of the given file + * stream to disk. + * + * ```ts + * const file = await Deno.open( + * "my_file.txt", + * { read: true, write: true, create: true }, + * ); + * await file.write(new TextEncoder().encode("Hello World")); + * await file.truncate(1); + * await file.sync(); + * console.log(await Deno.readTextFile("my_file.txt")); // H + * ``` + * + * @category I/O + */ + sync(): Promise; + /** + * Synchronously flushes any pending data and metadata operations of the given + * file stream to disk. + * + * ```ts + * const file = Deno.openSync( + * "my_file.txt", + * { read: true, write: true, create: true }, + * ); + * file.writeSync(new TextEncoder().encode("Hello World")); + * file.truncateSync(1); + * file.syncSync(); + * console.log(Deno.readTextFileSync("my_file.txt")); // H + * ``` + * + * @category I/O + */ + syncSync(): void; + /** + * Flushes any pending data operations of the given file stream to disk. + * ```ts + * using file = await Deno.open( + * "my_file.txt", + * { read: true, write: true, create: true }, + * ); + * await file.write(new TextEncoder().encode("Hello World")); + * await file.syncData(); + * console.log(await Deno.readTextFile("my_file.txt")); // Hello World + * ``` + * + * @category I/O + */ + syncData(): Promise; + /** + * Synchronously flushes any pending data operations of the given file stream + * to disk. + * + * ```ts + * using file = Deno.openSync( + * "my_file.txt", + * { read: true, write: true, create: true }, + * ); + * file.writeSync(new TextEncoder().encode("Hello World")); + * file.syncDataSync(); + * console.log(Deno.readTextFileSync("my_file.txt")); // Hello World + * ``` + * + * @category I/O + */ + syncDataSync(): void; + /** + * Changes the access (`atime`) and modification (`mtime`) times of the + * file stream resource. Given times are either in seconds (UNIX epoch + * time) or as `Date` objects. + * + * ```ts + * using file = await Deno.open("file.txt", { create: true, write: true }); + * await file.utime(1556495550, new Date()); + * ``` + * + * @category File System + */ + utime(atime: number | Date, mtime: number | Date): Promise; + /** + * Synchronously changes the access (`atime`) and modification (`mtime`) + * times of the file stream resource. Given times are either in seconds + * (UNIX epoch time) or as `Date` objects. + * + * ```ts + * using file = Deno.openSync("file.txt", { create: true, write: true }); + * file.utime(1556495550, new Date()); + * ``` + * + * @category File System + */ + utimeSync(atime: number | Date, mtime: number | Date): void; + /** **UNSTABLE**: New API, yet to be vetted. + * + * Checks if the file resource is a TTY (terminal). + * + * ```ts + * // This example is system and context specific + * using file = await Deno.open("/dev/tty6"); + * file.isTerminal(); // true + * ``` + */ + isTerminal(): boolean; + /** **UNSTABLE**: New API, yet to be vetted. + * + * Set TTY to be under raw mode or not. In raw mode, characters are read and + * returned as is, without being processed. All special processing of + * characters by the terminal is disabled, including echoing input + * characters. Reading from a TTY device in raw mode is faster than reading + * from a TTY device in canonical mode. + * + * ```ts + * using file = await Deno.open("/dev/tty6"); + * file.setRaw(true, { cbreak: true }); + * ``` + */ + setRaw(mode: boolean, options?: SetRawOptions): void; + /** + * Acquire an advisory file-system lock for the file. + * + * @param [exclusive=false] + */ + lock(exclusive?: boolean): Promise; + /** + * Synchronously acquire an advisory file-system lock synchronously for the file. + * + * @param [exclusive=false] + */ + lockSync(exclusive?: boolean): void; + /** + * Release an advisory file-system lock for the file. + */ + unlock(): Promise; + /** + * Synchronously release an advisory file-system lock for the file. + */ + unlockSync(): void; /** Close the file. Closing a file when you are finished with it is * important to avoid leaking resources. * * ```ts - * const file = await Deno.open("my_file.txt"); + * using file = await Deno.open("my_file.txt"); * // do work with "file" object - * file.close(); * ``` */ close(): void; @@ -2501,16 +2168,6 @@ declare namespace Deno { [Symbol.dispose](): void; } - /** - * The Deno abstraction for reading and writing files. - * - * @deprecated Use {@linkcode Deno.FsFile} instead. {@linkcode Deno.File} - * will be removed in the future. - * - * @category File System - */ - export const File: typeof FsFile; - /** Gets the size of the console as columns/rows. * * ```ts @@ -2540,9 +2197,12 @@ declare namespace Deno { } /** A reference to `stdin` which can be used to read directly from `stdin`. - * It implements the Deno specific {@linkcode Reader}, {@linkcode ReaderSync}, - * and {@linkcode Closer} interfaces as well as provides a - * {@linkcode ReadableStream} interface. + * + * It implements the Deno specific + * {@linkcode https://jsr.io/@std/io/doc/types/~/Reader | Reader}, + * {@linkcode https://jsr.io/@std/io/doc/types/~/ReaderSync | ReaderSync}, + * and {@linkcode https://jsr.io/@std/io/doc/types/~/Closer | Closer} + * interfaces as well as provides a {@linkcode ReadableStream} interface. * * ### Reading chunks from the readable stream * @@ -2556,10 +2216,57 @@ declare namespace Deno { * * @category I/O */ - export const stdin: Reader & ReaderSync & Closer & { - /** The resource ID assigned to `stdin`. This can be used with the discreet - * I/O functions in the `Deno` namespace. */ - readonly rid: number; + export const stdin: { + /** Read the incoming data from `stdin` into an array buffer (`p`). + * + * Resolves to either the number of bytes read during the operation or EOF + * (`null`) if there was nothing more to read. + * + * It is possible for a read to successfully return with `0` bytes. This + * does not indicate EOF. + * + * **It is not guaranteed that the full buffer will be read in a single + * call.** + * + * ```ts + * // If the text "hello world" is piped into the script: + * const buf = new Uint8Array(100); + * const numberOfBytesRead = await Deno.stdin.read(buf); // 11 bytes + * const text = new TextDecoder().decode(buf); // "hello world" + * ``` + * + * @category I/O + */ + read(p: Uint8Array): Promise; + /** Synchronously read from the incoming data from `stdin` into an array + * buffer (`p`). + * + * Returns either the number of bytes read during the operation or EOF + * (`null`) if there was nothing more to read. + * + * It is possible for a read to successfully return with `0` bytes. This + * does not indicate EOF. + * + * **It is not guaranteed that the full buffer will be read in a single + * call.** + * + * ```ts + * // If the text "hello world" is piped into the script: + * const buf = new Uint8Array(100); + * const numberOfBytesRead = Deno.stdin.readSync(buf); // 11 bytes + * const text = new TextDecoder().decode(buf); // "hello world" + * ``` + * + * @category I/O + */ + readSync(p: Uint8Array): number | null; + /** Closes `stdin`, freeing the resource. + * + * ```ts + * Deno.stdin.close(); + * ``` + */ + close(): void; /** A readable stream interface to `stdin`. */ readonly readable: ReadableStream; /** @@ -2576,10 +2283,23 @@ declare namespace Deno { * @category I/O */ setRaw(mode: boolean, options?: SetRawOptions): void; + /** + * Checks if `stdin` is a TTY (terminal). + * + * ```ts + * // This example is system and context specific + * Deno.stdin.isTerminal(); // true + * ``` + * + * @category I/O + */ + isTerminal(): boolean; }; /** A reference to `stdout` which can be used to write directly to `stdout`. - * It implements the Deno specific {@linkcode Writer}, {@linkcode WriterSync}, - * and {@linkcode Closer} interfaces as well as provides a + * It implements the Deno specific + * {@linkcode https://jsr.io/@std/io/doc/types/~/Writer | Writer}, + * {@linkcode https://jsr.io/@std/io/doc/types/~/WriterSync | WriterSync}, + * and {@linkcode https://jsr.io/@std/io/doc/types/~/Closer | Closer} interfaces as well as provides a * {@linkcode WritableStream} interface. * * These are low level constructs, and the {@linkcode console} interface is a @@ -2587,16 +2307,63 @@ declare namespace Deno { * * @category I/O */ - export const stdout: Writer & WriterSync & Closer & { - /** The resource ID assigned to `stdout`. This can be used with the discreet - * I/O functions in the `Deno` namespace. */ - readonly rid: number; + export const stdout: { + /** Write the contents of the array buffer (`p`) to `stdout`. + * + * Resolves to the number of bytes written. + * + * **It is not guaranteed that the full buffer will be written in a single + * call.** + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * const bytesWritten = await Deno.stdout.write(data); // 11 + * ``` + * + * @category I/O + */ + write(p: Uint8Array): Promise; + /** Synchronously write the contents of the array buffer (`p`) to `stdout`. + * + * Returns the number of bytes written. + * + * **It is not guaranteed that the full buffer will be written in a single + * call.** + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * const bytesWritten = Deno.stdout.writeSync(data); // 11 + * ``` + */ + writeSync(p: Uint8Array): number; + /** Closes `stdout`, freeing the resource. + * + * ```ts + * Deno.stdout.close(); + * ``` + */ + close(): void; /** A writable stream interface to `stdout`. */ readonly writable: WritableStream; + /** + * Checks if `stdout` is a TTY (terminal). + * + * ```ts + * // This example is system and context specific + * Deno.stdout.isTerminal(); // true + * ``` + * + * @category I/O + */ + isTerminal(): boolean; }; /** A reference to `stderr` which can be used to write directly to `stderr`. - * It implements the Deno specific {@linkcode Writer}, {@linkcode WriterSync}, - * and {@linkcode Closer} interfaces as well as provides a + * It implements the Deno specific + * {@linkcode https://jsr.io/@std/io/doc/types/~/Writer | Writer}, + * {@linkcode https://jsr.io/@std/io/doc/types/~/WriterSync | WriterSync}, + * and {@linkcode https://jsr.io/@std/io/doc/types/~/Closer | Closer} interfaces as well as provides a * {@linkcode WritableStream} interface. * * These are low level constructs, and the {@linkcode console} interface is a @@ -2604,12 +2371,57 @@ declare namespace Deno { * * @category I/O */ - export const stderr: Writer & WriterSync & Closer & { - /** The resource ID assigned to `stderr`. This can be used with the discreet - * I/O functions in the `Deno` namespace. */ - readonly rid: number; + export const stderr: { + /** Write the contents of the array buffer (`p`) to `stderr`. + * + * Resolves to the number of bytes written. + * + * **It is not guaranteed that the full buffer will be written in a single + * call.** + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * const bytesWritten = await Deno.stderr.write(data); // 11 + * ``` + * + * @category I/O + */ + write(p: Uint8Array): Promise; + /** Synchronously write the contents of the array buffer (`p`) to `stderr`. + * + * Returns the number of bytes written. + * + * **It is not guaranteed that the full buffer will be written in a single + * call.** + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * const bytesWritten = Deno.stderr.writeSync(data); // 11 + * ``` + */ + writeSync(p: Uint8Array): number; + /** Closes `stderr`, freeing the resource. + * + * ```ts + * Deno.stderr.close(); + * ``` + */ + close(): void; /** A writable stream interface to `stderr`. */ readonly writable: WritableStream; + /** + * Checks if `stderr` is a TTY (terminal). + * + * ```ts + * // This example is system and context specific + * Deno.stderr.isTerminal(); // true + * ``` + * + * @category I/O + */ + isTerminal(): boolean; }; /** @@ -2680,147 +2492,8 @@ declare namespace Deno { } /** - * Check if a given resource id (`rid`) is a TTY (a terminal). - * - * ```ts - * // This example is system and context specific - * const nonTTYRid = Deno.openSync("my_file.txt").rid; - * const ttyRid = Deno.openSync("/dev/tty6").rid; - * console.log(Deno.isatty(nonTTYRid)); // false - * console.log(Deno.isatty(ttyRid)); // true - * Deno.close(nonTTYRid); - * Deno.close(ttyRid); - * ``` - * - * @category I/O - */ - export function isatty(rid: number): boolean; - - /** - * A variable-sized buffer of bytes with `read()` and `write()` methods. - * - * @deprecated Use the - * [Web Streams API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API} - * instead. {@linkcode Deno.Buffer} will be removed in the future. - * - * @category I/O - */ - export class Buffer implements Reader, ReaderSync, Writer, WriterSync { - constructor(ab?: ArrayBuffer); - /** Returns a slice holding the unread portion of the buffer. - * - * The slice is valid for use only until the next buffer modification (that - * is, only until the next call to a method like `read()`, `write()`, - * `reset()`, or `truncate()`). If `options.copy` is false the slice aliases the buffer content at - * least until the next buffer modification, so immediate changes to the - * slice will affect the result of future reads. - * @param options Defaults to `{ copy: true }` - */ - bytes(options?: { copy?: boolean }): Uint8Array; - /** Returns whether the unread portion of the buffer is empty. */ - empty(): boolean; - /** A read only number of bytes of the unread portion of the buffer. */ - readonly length: number; - /** The read only capacity of the buffer's underlying byte slice, that is, - * the total space allocated for the buffer's data. */ - readonly capacity: number; - /** Discards all but the first `n` unread bytes from the buffer but - * continues to use the same allocated storage. It throws if `n` is - * negative or greater than the length of the buffer. */ - truncate(n: number): void; - /** Resets the buffer to be empty, but it retains the underlying storage for - * use by future writes. `.reset()` is the same as `.truncate(0)`. */ - reset(): void; - /** Reads the next `p.length` bytes from the buffer or until the buffer is - * drained. Returns the number of bytes read. If the buffer has no data to - * return, the return is EOF (`null`). */ - readSync(p: Uint8Array): number | null; - /** Reads the next `p.length` bytes from the buffer or until the buffer is - * drained. Resolves to the number of bytes read. If the buffer has no - * data to return, resolves to EOF (`null`). - * - * NOTE: This methods reads bytes synchronously; it's provided for - * compatibility with `Reader` interfaces. - */ - read(p: Uint8Array): Promise; - writeSync(p: Uint8Array): number; - /** NOTE: This methods writes bytes synchronously; it's provided for - * compatibility with `Writer` interface. */ - write(p: Uint8Array): Promise; - /** Grows the buffer's capacity, if necessary, to guarantee space for - * another `n` bytes. After `.grow(n)`, at least `n` bytes can be written to - * the buffer without another allocation. If `n` is negative, `.grow()` will - * throw. If the buffer can't grow it will throw an error. - * - * Based on Go Lang's - * [Buffer.Grow](https://golang.org/pkg/bytes/#Buffer.Grow). */ - grow(n: number): void; - /** Reads data from `r` until EOF (`null`) and appends it to the buffer, - * growing the buffer as needed. It resolves to the number of bytes read. - * If the buffer becomes too large, `.readFrom()` will reject with an error. - * - * Based on Go Lang's - * [Buffer.ReadFrom](https://golang.org/pkg/bytes/#Buffer.ReadFrom). */ - readFrom(r: Reader): Promise; - /** Reads data from `r` until EOF (`null`) and appends it to the buffer, - * growing the buffer as needed. It returns the number of bytes read. If the - * buffer becomes too large, `.readFromSync()` will throw an error. - * - * Based on Go Lang's - * [Buffer.ReadFrom](https://golang.org/pkg/bytes/#Buffer.ReadFrom). */ - readFromSync(r: ReaderSync): number; - } - - /** - * Read Reader `r` until EOF (`null`) and resolve to the content as - * Uint8Array`. - * - * @deprecated Use {@linkcode ReadableStream} and - * [`toArrayBuffer()`](https://deno.land/std/streams/to_array_buffer.ts?s=toArrayBuffer) - * instead. {@linkcode Deno.readAll} will be removed in the future. - * - * @category I/O - */ - export function readAll(r: Reader): Promise; - - /** - * Synchronously reads Reader `r` until EOF (`null`) and returns the content - * as `Uint8Array`. - * - * @deprecated Use {@linkcode ReadableStream} and - * [`toArrayBuffer()`](https://deno.land/std/streams/to_array_buffer.ts?s=toArrayBuffer) - * instead. {@linkcode Deno.readAllSync} will be removed in the future. - * - * @category I/O - */ - export function readAllSync(r: ReaderSync): Uint8Array; - - /** - * Write all the content of the array buffer (`arr`) to the writer (`w`). - * - * @deprecated Use {@linkcode WritableStream}, {@linkcode ReadableStream.from} - * and {@linkcode ReadableStream.pipeTo} instead. {@linkcode Deno.writeAll} - * will be removed in the future. - * - * @category I/O - */ - export function writeAll(w: Writer, arr: Uint8Array): Promise; - - /** - * Synchronously write all the content of the array buffer (`arr`) to the - * writer (`w`). - * - * @deprecated Use {@linkcode WritableStream}, {@linkcode ReadableStream.from} - * and {@linkcode ReadableStream.pipeTo} instead. - * {@linkcode Deno.writeAllSync} will be removed in the future. - * - * @category I/O - */ - export function writeAllSync(w: WriterSync, arr: Uint8Array): void; - - /** - * Options which can be set when using {@linkcode Deno.mkdir} and - * {@linkcode Deno.mkdirSync}. + * Options which can be set when using {@linkcode Deno.mkdir} and + * {@linkcode Deno.mkdirSync}. * * @category File System */ export interface MkdirOptions { @@ -3298,6 +2971,10 @@ declare namespace Deno { * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may * not be available on all platforms. */ birthtime: Date | null; + /** The last change time of the file. This corresponds to the `ctime` + * field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may + * not be available on all platforms. */ + ctime: Date | null; /** ID of the device containing the file. */ dev: number; /** Inode number. @@ -3306,8 +2983,7 @@ declare namespace Deno { ino: number | null; /** The underlying raw `st_mode` bits that contain the standard Unix * permissions for this file/directory. - * - * _Linux/Mac OS only._ */ + */ mode: number | null; /** Number of hard links pointing to this file. * @@ -3415,7 +3091,7 @@ declare namespace Deno { } /** Reads the directory given by `path` and returns an async iterable of - * {@linkcode Deno.DirEntry}. + * {@linkcode Deno.DirEntry}. The order of entries is not guaranteed. * * ```ts * for await (const dirEntry of Deno.readDir("/")) { @@ -3433,7 +3109,7 @@ declare namespace Deno { export function readDir(path: string | URL): AsyncIterable; /** Synchronously reads the directory given by `path` and returns an iterable - * of `Deno.DirEntry`. + * of {@linkcode Deno.DirEntry}. The order of entries is not guaranteed. * * ```ts * for (const dirEntry of Deno.readDirSync("/")) { @@ -3528,7 +3204,7 @@ declare namespace Deno { * of what it points to. * * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; + * import { assert } from "jsr:@std/assert"; * const fileInfo = await Deno.lstat("hello.txt"); * assert(fileInfo.isFile); * ``` @@ -3545,7 +3221,7 @@ declare namespace Deno { * returned instead of what it points to. * * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; + * import { assert } from "jsr:@std/assert"; * const fileInfo = Deno.lstatSync("hello.txt"); * assert(fileInfo.isFile); * ``` @@ -3561,7 +3237,7 @@ declare namespace Deno { * always follow symlinks. * * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; + * import { assert } from "jsr:@std/assert"; * const fileInfo = await Deno.stat("hello.txt"); * assert(fileInfo.isFile); * ``` @@ -3577,7 +3253,7 @@ declare namespace Deno { * `path`. Will always follow symlinks. * * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; + * import { assert } from "jsr:@std/assert"; * const fileInfo = Deno.statSync("hello.txt"); * assert(fileInfo.isFile); * ``` @@ -3717,7 +3393,7 @@ declare namespace Deno { * * ```ts * const file = await Deno.makeTempFile(); - * await Deno.writeFile(file, new TextEncoder().encode("Hello World")); + * await Deno.writeTextFile(file, "Hello World"); * await Deno.truncate(file, 7); * const data = await Deno.readFile(file); * console.log(new TextDecoder().decode(data)); // "Hello W" @@ -3757,9 +3433,9 @@ declare namespace Deno { */ export function truncateSync(name: string, len?: number): void; - /** @category Observability + /** @category Runtime * - * @deprecated This API has been deprecated in Deno v1.37.1. + * @deprecated This will be removed in Deno 2.0. */ export interface OpMetrics { opsDispatched: number; @@ -3775,69 +3451,6 @@ declare namespace Deno { bytesReceived: number; } - /** @category Observability - * - * @deprecated This API has been deprecated in Deno v1.37.1. - */ - export interface Metrics extends OpMetrics { - ops: Record; - } - - /** Receive metrics from the privileged side of Deno. This is primarily used - * in the development of Deno. _Ops_, also called _bindings_, are the - * go-between between Deno JavaScript sandbox and the rest of Deno. - * - * ```shell - * > console.table(Deno.metrics()) - * ┌─────────────────────────┬────────┐ - * │ (index) │ Values │ - * ├─────────────────────────┼────────┤ - * │ opsDispatched │ 3 │ - * │ opsDispatchedSync │ 2 │ - * │ opsDispatchedAsync │ 1 │ - * │ opsDispatchedAsyncUnref │ 0 │ - * │ opsCompleted │ 3 │ - * │ opsCompletedSync │ 2 │ - * │ opsCompletedAsync │ 1 │ - * │ opsCompletedAsyncUnref │ 0 │ - * │ bytesSentControl │ 73 │ - * │ bytesSentData │ 0 │ - * │ bytesReceived │ 375 │ - * └─────────────────────────┴────────┘ - * ``` - * - * @category Observability - * - * @deprecated This API has been deprecated in Deno v1.37.1. - */ - export function metrics(): Metrics; - - /** - * A map of open resources that Deno is tracking. The key is the resource ID - * (_rid_) and the value is its representation. - * - * @category Observability */ - interface ResourceMap { - [rid: number]: unknown; - } - - /** Returns a map of open resource IDs (_rid_) along with their string - * representations. This is an internal API and as such resource - * representation has `unknown` type; that means it can change any time and - * should not be depended upon. - * - * ```ts - * console.log(Deno.resources()); - * // { 0: "stdin", 1: "stdout", 2: "stderr" } - * Deno.openSync('../test.file'); - * console.log(Deno.resources()); - * // { 0: "stdin", 1: "stdout", 2: "stderr", 3: "fsFile" } - * ``` - * - * @category Observability - */ - export function resources(): ResourceMap; - /** * Additional information for FsEvent objects with the "other" kind. * @@ -3860,7 +3473,14 @@ declare namespace Deno { * @category File System */ export interface FsEvent { /** The kind/type of the file system event. */ - kind: "any" | "access" | "create" | "modify" | "remove" | "other"; + kind: + | "any" + | "access" + | "create" + | "modify" + | "rename" + | "remove" + | "other"; /** An array of paths that are associated with the file system event. */ paths: string[]; /** Any additional flags associated with the event. */ @@ -3875,14 +3495,10 @@ declare namespace Deno { * @category File System */ export interface FsWatcher extends AsyncIterable, Disposable { - /** The resource id. */ - readonly rid: number; /** Stops watching the file system and closes the watcher resource. */ close(): void; /** * Stops watching the file system and closes the watcher resource. - * - * @deprecated Will be removed in the future. */ return?(value?: any): Promise>; [Symbol.asyncIterator](): AsyncIterableIterator; @@ -3932,173 +3548,10 @@ declare namespace Deno { options?: { recursive: boolean }, ): FsWatcher; - /** - * @deprecated Use {@linkcode Deno.Command} instead. - * - * Options which can be used with {@linkcode Deno.run}. - * - * @category Sub Process */ - export interface RunOptions { - /** Arguments to pass. - * - * _Note_: the first element needs to be a path to the executable that is - * being run. */ - cmd: readonly string[] | [string | URL, ...string[]]; - /** The current working directory that should be used when running the - * sub-process. */ - cwd?: string; - /** Any environment variables to be set when running the sub-process. */ - env?: Record; - /** By default subprocess inherits `stdout` of parent process. To change - * this this option can be set to a resource ID (_rid_) of an open file, - * `"inherit"`, `"piped"`, or `"null"`: - * - * - _number_: the resource ID of an open file/resource. This allows you to - * write to a file. - * - `"inherit"`: The default if unspecified. The subprocess inherits from the - * parent. - * - `"piped"`: A new pipe should be arranged to connect the parent and child - * sub-process. - * - `"null"`: This stream will be ignored. This is the equivalent of attaching - * the stream to `/dev/null`. - */ - stdout?: "inherit" | "piped" | "null" | number; - /** By default subprocess inherits `stderr` of parent process. To change - * this this option can be set to a resource ID (_rid_) of an open file, - * `"inherit"`, `"piped"`, or `"null"`: - * - * - _number_: the resource ID of an open file/resource. This allows you to - * write to a file. - * - `"inherit"`: The default if unspecified. The subprocess inherits from the - * parent. - * - `"piped"`: A new pipe should be arranged to connect the parent and child - * sub-process. - * - `"null"`: This stream will be ignored. This is the equivalent of attaching - * the stream to `/dev/null`. - */ - stderr?: "inherit" | "piped" | "null" | number; - /** By default subprocess inherits `stdin` of parent process. To change - * this this option can be set to a resource ID (_rid_) of an open file, - * `"inherit"`, `"piped"`, or `"null"`: - * - * - _number_: the resource ID of an open file/resource. This allows you to - * read from a file. - * - `"inherit"`: The default if unspecified. The subprocess inherits from the - * parent. - * - `"piped"`: A new pipe should be arranged to connect the parent and child - * sub-process. - * - `"null"`: This stream will be ignored. This is the equivalent of attaching - * the stream to `/dev/null`. - */ - stdin?: "inherit" | "piped" | "null" | number; - } - - /** - * @deprecated Use {@linkcode Deno.Command} instead. - * - * The status resolved from the `.status()` method of a - * {@linkcode Deno.Process} instance. - * - * If `success` is `true`, then `code` will be `0`, but if `success` is - * `false`, the sub-process exit code will be set in `code`. - * - * @category Sub Process */ - export type ProcessStatus = - | { - success: true; - code: 0; - signal?: undefined; - } - | { - success: false; - code: number; - signal?: number; - }; - - /** - * * @deprecated Use {@linkcode Deno.Command} instead. - * - * Represents an instance of a sub process that is returned from - * {@linkcode Deno.run} which can be used to manage the sub-process. - * - * @category Sub Process */ - export class Process { - /** The resource ID of the sub-process. */ - readonly rid: number; - /** The operating system's process ID for the sub-process. */ - readonly pid: number; - /** A reference to the sub-processes `stdin`, which allows interacting with - * the sub-process at a low level. */ - readonly stdin: T["stdin"] extends "piped" ? Writer & Closer & { - writable: WritableStream; - } - : (Writer & Closer & { writable: WritableStream }) | null; - /** A reference to the sub-processes `stdout`, which allows interacting with - * the sub-process at a low level. */ - readonly stdout: T["stdout"] extends "piped" ? Reader & Closer & { - readable: ReadableStream; - } - : (Reader & Closer & { readable: ReadableStream }) | null; - /** A reference to the sub-processes `stderr`, which allows interacting with - * the sub-process at a low level. */ - readonly stderr: T["stderr"] extends "piped" ? Reader & Closer & { - readable: ReadableStream; - } - : (Reader & Closer & { readable: ReadableStream }) | null; - /** Wait for the process to exit and return its exit status. - * - * Calling this function multiple times will return the same status. - * - * The `stdin` reference to the process will be closed before waiting to - * avoid a deadlock. - * - * If `stdout` and/or `stderr` were set to `"piped"`, they must be closed - * manually before the process can exit. - * - * To run process to completion and collect output from both `stdout` and - * `stderr` use: - * - * ```ts - * const p = Deno.run({ cmd: [ "echo", "hello world" ], stderr: 'piped', stdout: 'piped' }); - * const [status, stdout, stderr] = await Promise.all([ - * p.status(), - * p.output(), - * p.stderrOutput() - * ]); - * p.close(); - * ``` - */ - status(): Promise; - /** Buffer the stdout until EOF and return it as `Uint8Array`. - * - * You must set `stdout` to `"piped"` when creating the process. - * - * This calls `close()` on stdout after its done. */ - output(): Promise; - /** Buffer the stderr until EOF and return it as `Uint8Array`. - * - * You must set `stderr` to `"piped"` when creating the process. - * - * This calls `close()` on stderr after its done. */ - stderrOutput(): Promise; - /** Clean up resources associated with the sub-process instance. */ - close(): void; - /** Send a signal to process. - * Default signal is `"SIGTERM"`. - * - * ```ts - * const p = Deno.run({ cmd: [ "sleep", "20" ]}); - * p.kill("SIGTERM"); - * p.close(); - * ``` - */ - kill(signo?: Signal): void; - } - /** Operating signals which can be listened for or sent to sub-processes. What * signals and what their standard behaviors are OS dependent. * - * @category Runtime Environment */ + * @category Runtime */ export type Signal = | "SIGABRT" | "SIGALRM" @@ -4113,6 +3566,8 @@ declare namespace Deno { | "SIGINFO" | "SIGINT" | "SIGIO" + | "SIGPOLL" + | "SIGUNUSED" | "SIGKILL" | "SIGPIPE" | "SIGPROF" @@ -4149,7 +3604,7 @@ declare namespace Deno { * _Note_: On Windows only `"SIGINT"` (CTRL+C) and `"SIGBREAK"` (CTRL+Break) * are supported. * - * @category Runtime Environment + * @category Runtime */ export function addSignalListener(signal: Signal, handler: () => void): void; @@ -4167,66 +3622,13 @@ declare namespace Deno { * _Note_: On Windows only `"SIGINT"` (CTRL+C) and `"SIGBREAK"` (CTRL+Break) * are supported. * - * @category Runtime Environment + * @category Runtime */ export function removeSignalListener( signal: Signal, handler: () => void, ): void; - /** - * @deprecated Use {@linkcode Deno.Command} instead. - * - * Spawns new subprocess. RunOptions must contain at a minimum the `opt.cmd`, - * an array of program arguments, the first of which is the binary. - * - * ```ts - * const p = Deno.run({ - * cmd: ["curl", "https://example.com"], - * }); - * const status = await p.status(); - * ``` - * - * Subprocess uses same working directory as parent process unless `opt.cwd` - * is specified. - * - * Environmental variables from parent process can be cleared using `opt.clearEnv`. - * Doesn't guarantee that only `opt.env` variables are present, - * as the OS may set environmental variables for processes. - * - * Environmental variables for subprocess can be specified using `opt.env` - * mapping. - * - * `opt.uid` sets the child process’s user ID. This translates to a setuid call - * in the child process. Failure in the setuid call will cause the spawn to fail. - * - * `opt.gid` is similar to `opt.uid`, but sets the group ID of the child process. - * This has the same semantics as the uid field. - * - * By default subprocess inherits stdio of parent process. To change - * this this, `opt.stdin`, `opt.stdout`, and `opt.stderr` can be set - * independently to a resource ID (_rid_) of an open file, `"inherit"`, - * `"piped"`, or `"null"`: - * - * - _number_: the resource ID of an open file/resource. This allows you to - * read or write to a file. - * - `"inherit"`: The default if unspecified. The subprocess inherits from the - * parent. - * - `"piped"`: A new pipe should be arranged to connect the parent and child - * sub-process. - * - `"null"`: This stream will be ignored. This is the equivalent of attaching - * the stream to `/dev/null`. - * - * Details of the spawned process are returned as an instance of - * {@linkcode Deno.Process}. - * - * Requires `allow-run` permission. - * - * @tags allow-run - * @category Sub Process - */ - export function run(opt: T): Process; - /** Create a child process. * * If any stdio options are not set to `"piped"`, accessing the corresponding @@ -4235,6 +3637,9 @@ declare namespace Deno { * If `stdin` is set to `"piped"`, the `stdin` {@linkcode WritableStream} * needs to be closed manually. * + * `Command` acts as a builder. Each call to {@linkcode Command.spawn} or + * {@linkcode Command.output} will spawn a new subprocess. + * * @example Spawn a subprocess and pipe the output to a file * * ```ts @@ -4289,15 +3694,13 @@ declare namespace Deno { * ``` * * @tags allow-run - * @category Sub Process + * @category Subprocess */ export class Command { constructor(command: string | URL, options?: CommandOptions); /** * Executes the {@linkcode Deno.Command}, waiting for it to finish and * collecting all of its output. - * If `spawn()` was called, calling this function will collect the remaining - * output. * * Will throw an error if `stdin: "piped"` is set. * @@ -4325,7 +3728,7 @@ declare namespace Deno { * The interface for handling a child process returned from * {@linkcode Deno.Command.spawn}. * - * @category Sub Process + * @category Subprocess */ export class ChildProcess implements AsyncDisposable { get stdin(): WritableStream; @@ -4359,7 +3762,7 @@ declare namespace Deno { /** * Options which can be set when calling {@linkcode Deno.Command}. * - * @category Sub Process + * @category Subprocess */ export interface CommandOptions { /** Arguments to pass to the process. */ @@ -4421,7 +3824,7 @@ declare namespace Deno { } /** - * @category Sub Process + * @category Subprocess */ export interface CommandStatus { /** If the child process exits with a 0 status code, `success` will be set @@ -4438,7 +3841,7 @@ declare namespace Deno { * {@linkcode Deno.Command.outputSync} which represents the result of spawning the * child process. * - * @category Sub Process + * @category Subprocess */ export interface CommandOutput extends CommandStatus { /** The buffered output from the child process' `stdout`. */ @@ -4449,7 +3852,7 @@ declare namespace Deno { /** Option which can be specified when performing {@linkcode Deno.inspect}. * - * @category Console and Debugging */ + * @category I/O */ export interface InspectOptions { /** Stylize output with ANSI colors. * @@ -4535,7 +3938,7 @@ declare namespace Deno { * Deno.inspect({a: {b: {c: {d: 'hello'}}}}, {depth: 2}); // { a: { b: [Object] } } * ``` * - * @category Console and Debugging + * @category I/O */ export function inspect(value: unknown, options?: InspectOptions): string; @@ -4550,8 +3953,7 @@ declare namespace Deno { | "net" | "env" | "sys" - | "ffi" - | "hrtime"; + | "ffi"; /** The current status of the permission: * @@ -4561,10 +3963,7 @@ declare namespace Deno { * * @category Permissions */ - export type PermissionState = - | "granted" - | "denied" - | "prompt"; + export type PermissionState = "granted" | "denied" | "prompt"; /** The permission descriptor for the `allow-run` and `deny-run` permissions, which controls * access to what sub-processes can be executed by Deno. The option `command` @@ -4660,12 +4059,18 @@ declare namespace Deno { | "osRelease" | "osUptime" | "uid" - | "gid"; + | "gid" + | "username" + | "cpus" + | "homedir" + | "statfs" + | "getPriority" + | "setPriority"; } /** The permission descriptor for the `allow-ffi` and `deny-ffi` permissions, which controls * access to loading _foreign_ code and interfacing with it via the - * [Foreign Function Interface API](https://deno.land/manual/runtime/ffi_api) + * [Foreign Function Interface API](https://docs.deno.com/runtime/manual/runtime/ffi_api) * available in Deno. The option `path` allows scoping the permission to a * specific path on the host. * @@ -4676,17 +4081,6 @@ declare namespace Deno { path?: string | URL; } - /** The permission descriptor for the `allow-hrtime` and `deny-hrtime` permissions, which - * controls if the runtime code has access to high resolution time. High - * resolution time is considered sensitive information, because it can be used - * by malicious code to gain information about the host that it might not - * otherwise have access to. - * - * @category Permissions */ - export interface HrtimePermissionDescriptor { - name: "hrtime"; - } - /** Permission descriptors which define a permission and can be queried, * requested, or revoked. * @@ -4702,15 +4096,14 @@ declare namespace Deno { | NetPermissionDescriptor | EnvPermissionDescriptor | SysPermissionDescriptor - | FfiPermissionDescriptor - | HrtimePermissionDescriptor; + | FfiPermissionDescriptor; /** The interface which defines what event types are supported by * {@linkcode PermissionStatus} instances. * * @category Permissions */ export interface PermissionStatusEventMap { - "change": Event; + change: Event; } /** An {@linkcode EventTarget} returned from the {@linkcode Deno.permissions} @@ -4809,7 +4202,7 @@ declare namespace Deno { /** Revokes a permission, and resolves to the state of the permission. * * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; + * import { assert } from "jsr:@std/assert"; * * const status = await Deno.permissions.revoke({ name: "run" }); * assert(status.state !== "granted") @@ -4820,7 +4213,7 @@ declare namespace Deno { /** Revokes a permission, and returns the state of the permission. * * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; + * import { assert } from "jsr:@std/assert"; * * const status = Deno.permissions.revokeSync({ name: "run" }); * assert(status.state !== "granted") @@ -4898,14 +4291,14 @@ declare namespace Deno { * ### Revoking * * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; + * import { assert } from "jsr:@std/assert"; * * const status = await Deno.permissions.revoke({ name: "run" }); * assert(status.state !== "granted") * ``` * * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; + * import { assert } from "jsr:@std/assert"; * * const status = Deno.permissions.revokeSync({ name: "run" }); * assert(status.state !== "granted") @@ -4944,7 +4337,7 @@ declare namespace Deno { * * The intended use for the information is for logging and debugging purposes. * - * @category Runtime Environment + * @category Runtime */ export const build: { /** The [LLVM](https://llvm.org/) target triple, which is the combination @@ -4958,6 +4351,7 @@ declare namespace Deno { os: | "darwin" | "linux" + | "android" | "windows" | "freebsd" | "netbsd" @@ -4979,7 +4373,7 @@ declare namespace Deno { * * The intended use for the information is for logging and debugging purposes. * - * @category Runtime Environment + * @category Runtime */ export const version: { /** Deno CLI's version. For example: `"1.26.0"`. */ @@ -5010,33 +4404,21 @@ declare namespace Deno { * [ "Sushi" ] * ``` * - * If you are looking for a structured way to parse arguments, there is the - * [`std/flags`](https://deno.land/std/flags) module as part of the Deno - * standard library. + * If you are looking for a structured way to parse arguments, there is + * [`parseArgs()`](https://jsr.io/@std/cli/doc/parse-args/~/parseArgs) from + * the Deno Standard Library. * - * @category Runtime Environment + * @category Runtime */ export const args: string[]; - /** - * A symbol which can be used as a key for a custom method which will be - * called when `Deno.inspect()` is called, or when the object is logged to - * the console. - * - * @deprecated This symbol is deprecated since 1.9. Use - * `Symbol.for("Deno.customInspect")` instead. - * - * @category Console and Debugging - */ - export const customInspect: unique symbol; - /** The URL of the entrypoint module entered from the command-line. It * requires read permission to the CWD. * * Also see {@linkcode ImportMeta} for other related information. * * @tags allow-read - * @category Runtime Environment + * @category Runtime */ export const mainModule: string; @@ -5045,16 +4427,16 @@ declare namespace Deno { * * @category File System */ export interface SymlinkOptions { - /** If the symbolic link should be either a file or directory. This option - * only applies to Windows and is ignored on other operating systems. */ - type: "file" | "dir"; + /** Specify the symbolic link type as file, directory or NTFS junction. This + * option only applies to Windows and is ignored on other operating systems. */ + type: "file" | "dir" | "junction"; } /** * Creates `newpath` as a symbolic link to `oldpath`. * - * The `options.type` parameter can be set to `"file"` or `"dir"`. This - * argument is only available on Windows and ignored on other platforms. + * The `options.type` parameter can be set to `"file"`, `"dir"` or `"junction"`. + * This argument is only available on Windows and ignored on other platforms. * * ```ts * await Deno.symlink("old/name", "new/name"); @@ -5074,8 +4456,8 @@ declare namespace Deno { /** * Creates `newpath` as a symbolic link to `oldpath`. * - * The `options.type` parameter can be set to `"file"` or `"dir"`. This - * argument is only available on Windows and ignored on other platforms. + * The `options.type` parameter can be set to `"file"`, `"dir"` or `"junction"`. + * This argument is only available on Windows and ignored on other platforms. * * ```ts * Deno.symlinkSync("old/name", "new/name"); @@ -5092,156 +4474,6 @@ declare namespace Deno { options?: SymlinkOptions, ): void; - /** - * Truncates or extends the specified file stream, to reach the specified - * `len`. - * - * If `len` is not specified then the entire file contents are truncated as if - * `len` was set to `0`. - * - * If the file previously was larger than this new length, the extra data is - * lost. - * - * If the file previously was shorter, it is extended, and the extended part - * reads as null bytes ('\0'). - * - * ### Truncate the entire file - * - * ```ts - * const file = await Deno.open( - * "my_file.txt", - * { read: true, write: true, create: true } - * ); - * await Deno.ftruncate(file.rid); - * ``` - * - * ### Truncate part of the file - * - * ```ts - * const file = await Deno.open( - * "my_file.txt", - * { read: true, write: true, create: true } - * ); - * await Deno.write(file.rid, new TextEncoder().encode("Hello World")); - * await Deno.ftruncate(file.rid, 7); - * const data = new Uint8Array(32); - * await Deno.read(file.rid, data); - * console.log(new TextDecoder().decode(data)); // Hello W - * ``` - * - * @category File System - */ - export function ftruncate(rid: number, len?: number): Promise; - - /** - * Synchronously truncates or extends the specified file stream, to reach the - * specified `len`. - * - * If `len` is not specified then the entire file contents are truncated as if - * `len` was set to `0`. - * - * If the file previously was larger than this new length, the extra data is - * lost. - * - * If the file previously was shorter, it is extended, and the extended part - * reads as null bytes ('\0'). - * - * ### Truncate the entire file - * - * ```ts - * const file = Deno.openSync( - * "my_file.txt", - * { read: true, write: true, truncate: true, create: true } - * ); - * Deno.ftruncateSync(file.rid); - * ``` - * - * ### Truncate part of the file - * - * ```ts - * const file = Deno.openSync( - * "my_file.txt", - * { read: true, write: true, create: true } - * ); - * Deno.writeSync(file.rid, new TextEncoder().encode("Hello World")); - * Deno.ftruncateSync(file.rid, 7); - * Deno.seekSync(file.rid, 0, Deno.SeekMode.Start); - * const data = new Uint8Array(32); - * Deno.readSync(file.rid, data); - * console.log(new TextDecoder().decode(data)); // Hello W - * ``` - * - * @category File System - */ - export function ftruncateSync(rid: number, len?: number): void; - - /** - * Synchronously changes the access (`atime`) and modification (`mtime`) times - * of a file stream resource referenced by `rid`. Given times are either in - * seconds (UNIX epoch time) or as `Date` objects. - * - * ```ts - * const file = Deno.openSync("file.txt", { create: true, write: true }); - * Deno.futimeSync(file.rid, 1556495550, new Date()); - * ``` - * - * @category File System - */ - export function futimeSync( - rid: number, - atime: number | Date, - mtime: number | Date, - ): void; - - /** - * Changes the access (`atime`) and modification (`mtime`) times of a file - * stream resource referenced by `rid`. Given times are either in seconds - * (UNIX epoch time) or as `Date` objects. - * - * ```ts - * const file = await Deno.open("file.txt", { create: true, write: true }); - * await Deno.futime(file.rid, 1556495550, new Date()); - * ``` - * - * @category File System - */ - export function futime( - rid: number, - atime: number | Date, - mtime: number | Date, - ): Promise; - - /** - * Returns a `Deno.FileInfo` for the given file stream. - * - * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; - * - * const file = await Deno.open("file.txt", { read: true }); - * const fileInfo = await Deno.fstat(file.rid); - * assert(fileInfo.isFile); - * ``` - * - * @category File System - */ - export function fstat(rid: number): Promise; - - /** - * Synchronously returns a {@linkcode Deno.FileInfo} for the given file - * stream. - * - * ```ts - * import { assert } from "https://deno.land/std/assert/mod.ts"; - * - * const file = Deno.openSync("file.txt", { read: true }); - * const fileInfo = Deno.fstatSync(file.rid); - * assert(fileInfo.isFile); - * ``` - * - * @category File System - */ - export function fstatSync(rid: number): FileInfo; - /** * Synchronously changes the access (`atime`) and modification (`mtime`) times * of a file system object referenced by `path`. Given times are either in @@ -5282,100 +4514,28 @@ declare namespace Deno { mtime: number | Date, ): Promise; - /** The event yielded from an {@linkcode HttpConn} which represents an HTTP - * request from a remote client. - * - * @category HTTP Server */ - export interface RequestEvent { - /** The request from the client in the form of the web platform - * {@linkcode Request}. */ - readonly request: Request; - /** The method to be used to respond to the event. The response needs to - * either be an instance of {@linkcode Response} or a promise that resolves - * with an instance of `Response`. - * - * When the response is successfully processed then the promise returned - * will be resolved. If there are any issues with sending the response, - * the promise will be rejected. */ - respondWith(r: Response | PromiseLike): Promise; - } - - /** The async iterable that is returned from {@linkcode Deno.serveHttp} which - * yields up {@linkcode RequestEvent} events, representing individual - * requests on the HTTP server connection. - * - * @category HTTP Server */ - export interface HttpConn extends AsyncIterable, Disposable { - /** The resource ID associated with this connection. Generally users do not - * need to be aware of this identifier. */ - readonly rid: number; - - /** An alternative to the async iterable interface which provides promises - * which resolve with either a {@linkcode RequestEvent} when there is - * another request or `null` when the client has closed the connection. */ - nextRequest(): Promise; - /** Initiate a server side closure of the connection, indicating to the - * client that you refuse to accept any more requests on this connection. - * - * Typically the client closes the connection, which will result in the - * async iterable terminating or the `nextRequest()` method returning - * `null`. */ - close(): void; - } - - /** - * Provides an interface to handle HTTP request and responses over TCP or TLS - * connections. The method returns an {@linkcode HttpConn} which yields up - * {@linkcode RequestEvent} events, which utilize the web platform standard - * {@linkcode Request} and {@linkcode Response} objects to handle the request. - * - * ```ts - * const conn = Deno.listen({ port: 80 }); - * const httpConn = Deno.serveHttp(await conn.accept()); - * const e = await httpConn.nextRequest(); - * if (e) { - * e.respondWith(new Response("Hello World")); - * } - * ``` - * - * Alternatively, you can also use the async iterator approach: + /** Retrieve the process umask. If `mask` is provided, sets the process umask. + * This call always returns what the umask was before the call. * * ```ts - * async function handleHttp(conn: Deno.Conn) { - * for await (const e of Deno.serveHttp(conn)) { - * e.respondWith(new Response("Hello World")); - * } - * } - * - * for await (const conn of Deno.listen({ port: 80 })) { - * handleHttp(conn); - * } + * console.log(Deno.umask()); // e.g. 18 (0o022) + * const prevUmaskValue = Deno.umask(0o077); // e.g. 18 (0o022) + * console.log(Deno.umask()); // e.g. 63 (0o077) * ``` * - * If `httpConn.nextRequest()` encounters an error or returns `null` then the - * underlying {@linkcode HttpConn} resource is closed automatically. - * - * Also see the experimental Flash HTTP server {@linkcode Deno.serve} which - * provides a ground up rewrite of handling of HTTP requests and responses - * within the Deno CLI. - * - * Note that this function *consumes* the given connection passed to it, thus - * the original connection will be unusable after calling this. Additionally, - * you need to ensure that the connection is not being used elsewhere when - * calling this function in order for the connection to be consumed properly. + * This API is under consideration to determine if permissions are required to + * call it. * - * For instance, if there is a `Promise` that is waiting for read operation on - * the connection to complete, it is considered that the connection is being - * used elsewhere. In such a case, this function will fail. + * *Note*: This API is not implemented on Windows * - * @category HTTP Server + * @category File System */ - export function serveHttp(conn: Conn): HttpConn; + export function umask(mask?: number): number; /** The object that is returned from a {@linkcode Deno.upgradeWebSocket} * request. * - * @category Web Sockets */ + * @category WebSockets */ export interface WebSocketUpgrade { /** The response object that represents the HTTP response to the client, * which should be used to the {@linkcode RequestEvent} `.respondWith()` for @@ -5389,7 +4549,7 @@ declare namespace Deno { /** Options which can be set when performing a * {@linkcode Deno.upgradeWebSocket} upgrade of a {@linkcode Request} * - * @category Web Sockets */ + * @category WebSockets */ export interface UpgradeWebSocketOptions { /** Sets the `.protocol` property on the client side web socket to the * value provided here, which should be one of the strings specified in the @@ -5401,7 +4561,8 @@ declare namespace Deno { * `pong` within the timeout specified, the connection is deemed * unhealthy and is closed. The `close` and `error` event will be emitted. * - * The default is 120 seconds. Set to `0` to disable timeouts. */ + * The unit is seconds, with a default of 30. + * Set to `0` to disable timeouts. */ idleTimeout?: number; } @@ -5413,22 +4574,21 @@ declare namespace Deno { * with the returned response for the websocket upgrade to be successful. * * ```ts - * const conn = Deno.listen({ port: 80 }); - * const httpConn = Deno.serveHttp(await conn.accept()); - * const e = await httpConn.nextRequest(); - * if (e) { - * const { socket, response } = Deno.upgradeWebSocket(e.request); - * socket.onopen = () => { - * socket.send("Hello World!"); - * }; - * socket.onmessage = (e) => { - * console.log(e.data); - * socket.close(); - * }; - * socket.onclose = () => console.log("WebSocket has been closed."); - * socket.onerror = (e) => console.error("WebSocket error:", e); - * e.respondWith(response); - * } + * Deno.serve((req) => { + * if (req.headers.get("upgrade") !== "websocket") { + * return new Response(null, { status: 501 }); + * } + * const { socket, response } = Deno.upgradeWebSocket(req); + * socket.addEventListener("open", () => { + * console.log("a client connected!"); + * }); + * socket.addEventListener("message", (event) => { + * if (event.data === "ping") { + * socket.send("pong"); + * } + * }); + * return response; + * }); * ``` * * If the request body is disturbed (read from) before the upgrade is @@ -5437,7 +4597,7 @@ declare namespace Deno { * This operation does not yet consume the request or open the websocket. This * only happens once the returned response has been passed to `respondWith()`. * - * @category Web Sockets + * @category WebSockets */ export function upgradeWebSocket( request: Request, @@ -5457,17 +4617,16 @@ declare namespace Deno { * Windows. * * ```ts - * const p = Deno.run({ - * cmd: ["sleep", "10000"] - * }); + * const command = new Deno.Command("sleep", { args: ["10000"] }); + * const child = command.spawn(); * - * Deno.kill(p.pid, "SIGINT"); + * Deno.kill(child.pid, "SIGINT"); * ``` * * Requires `allow-run` permission. * * @tags allow-run - * @category Sub Process + * @category Subprocess */ export function kill(pid: number, signo?: Signal): void; @@ -5522,7 +4681,7 @@ declare namespace Deno { * * @category Network */ - export interface CAARecord { + export interface CaaRecord { /** If `true`, indicates that the corresponding property tag **must** be * understood if the semantics of the CAA record are to be correctly * interpreted by an issuer. @@ -5542,7 +4701,7 @@ declare namespace Deno { * specified, it will return an array of objects with this interface. * * @category Network */ - export interface MXRecord { + export interface MxRecord { /** A priority value, which is a relative value compared to the other * preferences of MX records for the domain. */ preference: number; @@ -5554,7 +4713,7 @@ declare namespace Deno { * specified, it will return an array of objects with this interface. * * @category Network */ - export interface NAPTRRecord { + export interface NaptrRecord { order: number; preference: number; flags: string; @@ -5567,7 +4726,7 @@ declare namespace Deno { * specified, it will return an array of objects with this interface. * * @category Network */ - export interface SOARecord { + export interface SoaRecord { mname: string; rname: string; serial: number; @@ -5582,7 +4741,7 @@ declare namespace Deno { * * @category Network */ - export interface SRVRecord { + export interface SrvRecord { priority: number; weight: number; port: number; @@ -5647,7 +4806,7 @@ declare namespace Deno { query: string, recordType: "CAA", options?: ResolveDnsOptions, - ): Promise; + ): Promise; /** * Performs DNS resolution against the given query, returning resolved @@ -5677,7 +4836,7 @@ declare namespace Deno { query: string, recordType: "MX", options?: ResolveDnsOptions, - ): Promise; + ): Promise; /** * Performs DNS resolution against the given query, returning resolved @@ -5707,7 +4866,7 @@ declare namespace Deno { query: string, recordType: "NAPTR", options?: ResolveDnsOptions, - ): Promise; + ): Promise; /** * Performs DNS resolution against the given query, returning resolved @@ -5737,7 +4896,7 @@ declare namespace Deno { query: string, recordType: "SOA", options?: ResolveDnsOptions, - ): Promise; + ): Promise; /** * Performs DNS resolution against the given query, returning resolved @@ -5767,7 +4926,7 @@ declare namespace Deno { query: string, recordType: "SRV", options?: ResolveDnsOptions, - ): Promise; + ): Promise; /** * Performs DNS resolution against the given query, returning resolved @@ -5829,25 +4988,25 @@ declare namespace Deno { options?: ResolveDnsOptions, ): Promise< | string[] - | CAARecord[] - | MXRecord[] - | NAPTRRecord[] - | SOARecord[] - | SRVRecord[] + | CaaRecord[] + | MxRecord[] + | NaptrRecord[] + | SoaRecord[] + | SrvRecord[] | string[][] >; /** * Make the timer of the given `id` block the event loop from finishing. * - * @category Timers + * @category Runtime */ export function refTimer(id: number): void; /** * Make the timer of the given `id` not block the event loop from finishing. * - * @category Timers + * @category Runtime */ export function unrefTimer(id: number): void; @@ -5861,7 +5020,7 @@ declare namespace Deno { * Requires `allow-sys` permission. * * @tags allow-sys - * @category Runtime Environment + * @category Runtime */ export function uid(): number | null; @@ -5875,17 +5034,19 @@ declare namespace Deno { * Requires `allow-sys` permission. * * @tags allow-sys - * @category Runtime Environment + * @category Runtime */ export function gid(): number | null; - /** Information for a HTTP request. + /** Additional information for an HTTP request and its connection. * * @category HTTP Server */ - export interface ServeHandlerInfo { + export interface ServeHandlerInfo { /** The remote address of the connection. */ - remoteAddr: Deno.NetAddr; + remoteAddr: Addr; + /** The completion promise */ + completed: Promise; } /** A handler for HTTP requests. Consumes a request and returns a response. @@ -5896,17 +5057,66 @@ declare namespace Deno { * * @category HTTP Server */ - export type ServeHandler = ( + export type ServeHandler = ( request: Request, - info: ServeHandlerInfo, + info: ServeHandlerInfo, ) => Response | Promise; + /** Interface that module run with `deno serve` subcommand must conform to. + * + * To ensure your code is type-checked properly, make sure to add `satisfies Deno.ServeDefaultExport` + * to the `export default { ... }` like so: + * + * ```ts + * export default { + * fetch(req) { + * return new Response("Hello world"); + * } + * } satisfies Deno.ServeDefaultExport; + * ``` + * + * @category HTTP Server + */ + export interface ServeDefaultExport { + /** A handler for HTTP requests. Consumes a request and returns a response. + * + * If a handler throws, the server calling the handler will assume the impact + * of the error is isolated to the individual request. It will catch the error + * and if necessary will close the underlying connection. + * + * @category HTTP Server + */ + fetch: ServeHandler; + } + /** Options which can be set when calling {@linkcode Deno.serve}. * * @category HTTP Server */ - export interface ServeOptions { + export interface ServeOptions { + /** An {@linkcode AbortSignal} to close the server and all connections. */ + signal?: AbortSignal; + + /** The handler to invoke when route handlers throw an error. */ + onError?: (error: unknown) => Response | Promise; + + /** The callback which is called when the server starts listening. */ + onListen?: (localAddr: Addr) => void; + } + + /** + * Options that can be passed to `Deno.serve` to create a server listening on + * a TCP port. + * + * @category HTTP Server + */ + export interface ServeTcpOptions extends ServeOptions { + /** The transport to use. */ + transport?: "tcp"; + /** The port to listen on. + * + * Set to `0` to listen on any available port. * * @default {8000} */ port?: number; @@ -5921,93 +5131,46 @@ declare namespace Deno { * @default {"0.0.0.0"} */ hostname?: string; - /** An {@linkcode AbortSignal} to close the server and all connections. */ - signal?: AbortSignal; - /** Sets `SO_REUSEPORT` on POSIX systems. */ reusePort?: boolean; - - /** The handler to invoke when route handlers throw an error. */ - onError?: (error: unknown) => Response | Promise; - - /** The callback which is called when the server starts listening. */ - onListen?: (params: { hostname: string; port: number }) => void; - } - - /** Additional options which are used when opening a TLS (HTTPS) server. - * - * @category HTTP Server - */ - export interface ServeTlsOptions extends ServeOptions { - /** Server private key in PEM format */ - cert: string; - - /** Cert chain in PEM format */ - key: string; } /** + * Options that can be passed to `Deno.serve` to create a server listening on + * a Unix domain socket. + * * @category HTTP Server */ - export interface ServeInit { - /** The handler to invoke to process each incoming request. */ - handler: ServeHandler; - } + export interface ServeUnixOptions extends ServeOptions { + /** The transport to use. */ + transport?: "unix"; - export interface ServeUnixOptions { /** The unix domain socket path to listen on. */ path: string; - - /** An {@linkcode AbortSignal} to close the server and all connections. */ - signal?: AbortSignal; - - /** The handler to invoke when route handlers throw an error. */ - onError?: (error: unknown) => Response | Promise; - - /** The callback which is called when the server starts listening. */ - onListen?: (params: { path: string }) => void; - } - - /** Information for a unix domain socket HTTP request. - * - * @category HTTP Server - */ - export interface ServeUnixHandlerInfo { - /** The remote address of the connection. */ - remoteAddr: Deno.UnixAddr; } - /** A handler for unix domain socket HTTP requests. Consumes a request and returns a response. - * - * If a handler throws, the server calling the handler will assume the impact - * of the error is isolated to the individual request. It will catch the error - * and if necessary will close the underlying connection. - * - * @category HTTP Server - */ - export type ServeUnixHandler = ( - request: Request, - info: ServeUnixHandlerInfo, - ) => Response | Promise; - /** * @category HTTP Server */ - export interface ServeUnixInit { + export interface ServeInit { /** The handler to invoke to process each incoming request. */ - handler: ServeUnixHandler; + handler: ServeHandler; } /** An instance of the server created using `Deno.serve()` API. * * @category HTTP Server */ - export interface HttpServer extends AsyncDisposable { + export interface HttpServer + extends AsyncDisposable { /** A promise that resolves once server finishes - eg. when aborted using * the signal passed to {@linkcode ServeOptions.signal}. */ finished: Promise; + /** The local address this server is listening on. */ + addr: Addr; + /** * Make the server block the event loop from finishing. * @@ -6025,12 +5188,6 @@ declare namespace Deno { shutdown(): Promise; } - /** - * @category HTTP Server - * @deprecated Use {@linkcode HttpServer} instead. - */ - export type Server = HttpServer; - /** Serves HTTP requests with the given handler. * * The below example serves with the port `8000` on hostname `"127.0.0.1"`. @@ -6041,7 +5198,9 @@ declare namespace Deno { * * @category HTTP Server */ - export function serve(handler: ServeHandler): HttpServer; + export function serve( + handler: ServeHandler, + ): HttpServer; /** Serves HTTP requests with the given option bag and handler. * * You can specify the socket path with `path` option. @@ -6089,19 +5248,19 @@ declare namespace Deno { */ export function serve( options: ServeUnixOptions, - handler: ServeUnixHandler, - ): HttpServer; + handler: ServeHandler, + ): HttpServer; /** Serves HTTP requests with the given option bag and handler. * * You can specify an object with a port and hostname option, which is the - * address to listen on. The default is port `8000` on hostname `"127.0.0.1"`. + * address to listen on. The default is port `8000` on hostname `"0.0.0.0"`. * * You can change the address to listen on using the `hostname` and `port` - * options. The below example serves on port `3000` and hostname `"0.0.0.0"`. + * options. The below example serves on port `3000` and hostname `"127.0.0.1"`. * * ```ts * Deno.serve( - * { port: 3000, hostname: "0.0.0.0" }, + * { port: 3000, hostname: "127.0.0.1" }, * (_req) => new Response("Hello, world") * ); * ``` @@ -6148,9 +5307,9 @@ declare namespace Deno { * @category HTTP Server */ export function serve( - options: ServeOptions | ServeTlsOptions, - handler: ServeHandler, - ): HttpServer; + options: ServeTcpOptions | (ServeTcpOptions & TlsCertifiedKeyPem), + handler: ServeHandler, + ): HttpServer; /** Serves HTTP requests with the given option bag. * * You can specify an object with the path option, which is the @@ -6176,19 +5335,19 @@ declare namespace Deno { * @category HTTP Server */ export function serve( - options: ServeUnixInit & ServeUnixOptions, - ): HttpServer; + options: ServeUnixOptions & ServeInit, + ): HttpServer; /** Serves HTTP requests with the given option bag. * * You can specify an object with a port and hostname option, which is the - * address to listen on. The default is port `8000` on hostname `"127.0.0.1"`. + * address to listen on. The default is port `8000` on hostname `"0.0.0.0"`. * * ```ts * const ac = new AbortController(); * * const server = Deno.serve({ * port: 3000, - * hostname: "0.0.0.0", + * hostname: "127.0.0.1", * handler: (_req) => new Response("Hello, world"), * signal: ac.signal, * onListen({ port, hostname }) { @@ -6204,441 +5363,1260 @@ declare namespace Deno { * @category HTTP Server */ export function serve( - options: ServeInit & (ServeOptions | ServeTlsOptions), - ): HttpServer; -} - -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file no-explicit-any - -/// -/// - -/** @category Console and Debugging */ -declare interface Console { - assert(condition?: boolean, ...data: any[]): void; - clear(): void; - count(label?: string): void; - countReset(label?: string): void; - debug(...data: any[]): void; - dir(item?: any, options?: any): void; - dirxml(...data: any[]): void; - error(...data: any[]): void; - group(...data: any[]): void; - groupCollapsed(...data: any[]): void; - groupEnd(): void; - info(...data: any[]): void; - log(...data: any[]): void; - table(tabularData?: any, properties?: string[]): void; - time(label?: string): void; - timeEnd(label?: string): void; - timeLog(label?: string, ...data: any[]): void; - trace(...data: any[]): void; - warn(...data: any[]): void; - - /** This method is a noop, unless used in inspector */ - timeStamp(label?: string): void; - - /** This method is a noop, unless used in inspector */ - profile(label?: string): void; - - /** This method is a noop, unless used in inspector */ - profileEnd(label?: string): void; -} - -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file no-explicit-any no-var + options: + & (ServeTcpOptions | (ServeTcpOptions & TlsCertifiedKeyPem)) + & ServeInit, + ): HttpServer; -/// -/// - -/** @category Web APIs */ -declare interface URLSearchParams { - /** Appends a specified key/value pair as a new search parameter. + /** All plain number types for interfacing with foreign functions. * - * ```ts - * let searchParams = new URLSearchParams(); - * searchParams.append('name', 'first'); - * searchParams.append('name', 'second'); - * ``` + * @category FFI */ - append(name: string, value: string): void; + export type NativeNumberType = + | "u8" + | "i8" + | "u16" + | "i16" + | "u32" + | "i32" + | "f32" + | "f64"; - /** Deletes search parameters that match a name, and optional value, - * from the list of all search parameters. + /** All BigInt number types for interfacing with foreign functions. * - * ```ts - * let searchParams = new URLSearchParams([['name', 'value']]); - * searchParams.delete('name'); - * searchParams.delete('name', 'value'); - * ``` + * @category FFI */ - delete(name: string, value?: string): void; + export type NativeBigIntType = "u64" | "i64" | "usize" | "isize"; - /** Returns all the values associated with a given search parameter - * as an array. + /** The native boolean type for interfacing to foreign functions. * - * ```ts - * searchParams.getAll('name'); - * ``` + * @category FFI */ - getAll(name: string): string[]; + export type NativeBooleanType = "bool"; - /** Returns the first value associated to the given search parameter. + /** The native pointer type for interfacing to foreign functions. * - * ```ts - * searchParams.get('name'); - * ``` + * @category FFI */ - get(name: string): string | null; + export type NativePointerType = "pointer"; - /** Returns a boolean value indicating if a given parameter, - * or parameter and value pair, exists. + /** The native buffer type for interfacing to foreign functions. * - * ```ts - * searchParams.has('name'); - * searchParams.has('name', 'value'); - * ``` + * @category FFI */ - has(name: string, value?: string): boolean; + export type NativeBufferType = "buffer"; - /** Sets the value associated with a given search parameter to the - * given value. If there were several matching values, this method - * deletes the others. If the search parameter doesn't exist, this - * method creates it. + /** The native function type for interfacing with foreign functions. * - * ```ts - * searchParams.set('name', 'value'); - * ``` + * @category FFI */ - set(name: string, value: string): void; + export type NativeFunctionType = "function"; - /** Sort all key/value pairs contained in this object in place and - * return undefined. The sort order is according to Unicode code - * points of the keys. + /** The native void type for interfacing with foreign functions. * - * ```ts - * searchParams.sort(); - * ``` + * @category FFI */ - sort(): void; + export type NativeVoidType = "void"; - /** Calls a function for each element contained in this object in - * place and return undefined. Optionally accepts an object to use - * as this when executing callback as second argument. + /** The native struct type for interfacing with foreign functions. * - * ```ts - * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); - * params.forEach((value, key, parent) => { - * console.log(value, key, parent); - * }); - * ``` + * @category FFI */ - forEach( - callbackfn: (value: string, key: string, parent: this) => void, - thisArg?: any, - ): void; + export interface NativeStructType { + readonly struct: readonly NativeType[]; + } - /** Returns an iterator allowing to go through all keys contained - * in this object. - * - * ```ts - * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); - * for (const key of params.keys()) { - * console.log(key); - * } - * ``` + /** + * @category FFI */ - keys(): IterableIterator; + export const brand: unique symbol; - /** Returns an iterator allowing to go through all values contained - * in this object. - * - * ```ts - * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); - * for (const value of params.values()) { - * console.log(value); - * } - * ``` + /** + * @category FFI */ - values(): IterableIterator; - - /** Returns an iterator allowing to go through all key/value - * pairs contained in this object. - * - * ```ts - * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); - * for (const [key, value] of params.entries()) { - * console.log(key, value); - * } - * ``` + export type NativeU8Enum = "u8" & { [brand]: T }; + /** + * @category FFI */ - entries(): IterableIterator<[string, string]>; - - /** Returns an iterator allowing to go through all key/value - * pairs contained in this object. - * - * ```ts - * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); - * for (const [key, value] of params) { - * console.log(key, value); - * } - * ``` + export type NativeI8Enum = "i8" & { [brand]: T }; + /** + * @category FFI */ - [Symbol.iterator](): IterableIterator<[string, string]>; + export type NativeU16Enum = "u16" & { [brand]: T }; + /** + * @category FFI + */ + export type NativeI16Enum = "i16" & { [brand]: T }; + /** + * @category FFI + */ + export type NativeU32Enum = "u32" & { [brand]: T }; + /** + * @category FFI + */ + export type NativeI32Enum = "i32" & { [brand]: T }; + /** + * @category FFI + */ + export type NativeTypedPointer = "pointer" & { + [brand]: T; + }; + /** + * @category FFI + */ + export type NativeTypedFunction = + & "function" + & { + [brand]: T; + }; - /** Returns a query string suitable for use in a URL. + /** All supported types for interfacing with foreign functions. * - * ```ts - * searchParams.toString(); - * ``` + * @category FFI */ - toString(): string; + export type NativeType = + | NativeNumberType + | NativeBigIntType + | NativeBooleanType + | NativePointerType + | NativeBufferType + | NativeFunctionType + | NativeStructType; - /** Contains the number of search parameters - * - * ```ts - * searchParams.size - * ``` + /** @category FFI */ - size: number; -} + export type NativeResultType = NativeType | NativeVoidType; -/** @category Web APIs */ -declare var URLSearchParams: { - readonly prototype: URLSearchParams; - new ( - init?: Iterable | Record | string, - ): URLSearchParams; -}; + /** Type conversion for foreign symbol parameters and unsafe callback return + * types. + * + * @category FFI + */ + export type ToNativeType = T extends + NativeStructType ? BufferSource + : T extends NativeNumberType ? T extends NativeU8Enum ? U + : T extends NativeI8Enum ? U + : T extends NativeU16Enum ? U + : T extends NativeI16Enum ? U + : T extends NativeU32Enum ? U + : T extends NativeI32Enum ? U + : number + : T extends NativeBigIntType ? bigint + : T extends NativeBooleanType ? boolean + : T extends NativePointerType + ? T extends NativeTypedPointer ? U | null + : PointerValue + : T extends NativeFunctionType + ? T extends NativeTypedFunction ? PointerValue | null + : PointerValue + : T extends NativeBufferType ? BufferSource | null + : never; -/** The URL interface represents an object providing static methods used for - * creating object URLs. - * - * @category Web APIs - */ -declare interface URL { - hash: string; - host: string; - hostname: string; - href: string; - toString(): string; - readonly origin: string; - password: string; - pathname: string; - port: string; - protocol: string; - search: string; - readonly searchParams: URLSearchParams; - username: string; - toJSON(): string; -} + /** Type conversion for unsafe callback return types. + * + * @category FFI + */ + export type ToNativeResultType< + T extends NativeResultType = NativeResultType, + > = T extends NativeStructType ? BufferSource + : T extends NativeNumberType ? T extends NativeU8Enum ? U + : T extends NativeI8Enum ? U + : T extends NativeU16Enum ? U + : T extends NativeI16Enum ? U + : T extends NativeU32Enum ? U + : T extends NativeI32Enum ? U + : number + : T extends NativeBigIntType ? bigint + : T extends NativeBooleanType ? boolean + : T extends NativePointerType + ? T extends NativeTypedPointer ? U | null + : PointerValue + : T extends NativeFunctionType + ? T extends NativeTypedFunction ? PointerObject | null + : PointerValue + : T extends NativeBufferType ? BufferSource | null + : T extends NativeVoidType ? void + : never; -/** The URL interface represents an object providing static methods used for - * creating object URLs. - * - * @category Web APIs - */ -declare var URL: { - readonly prototype: URL; - new (url: string | URL, base?: string | URL): URL; - canParse(url: string | URL, base?: string | URL): boolean; - createObjectURL(blob: Blob): string; - revokeObjectURL(url: string): void; -}; + /** A utility type for conversion of parameter types of foreign functions. + * + * @category FFI + */ + export type ToNativeParameterTypes = + // + [T[number][]] extends [T] ? ToNativeType[] + : [readonly T[number][]] extends [T] ? readonly ToNativeType[] + : T extends readonly [...NativeType[]] ? { + [K in keyof T]: ToNativeType; + } + : never; -/** @category Web APIs */ -declare interface URLPatternInit { - protocol?: string; - username?: string; - password?: string; - hostname?: string; - port?: string; - pathname?: string; - search?: string; - hash?: string; - baseURL?: string; -} + /** Type conversion for foreign symbol return types and unsafe callback + * parameters. + * + * @category FFI + */ + export type FromNativeType = T extends + NativeStructType ? Uint8Array + : T extends NativeNumberType ? T extends NativeU8Enum ? U + : T extends NativeI8Enum ? U + : T extends NativeU16Enum ? U + : T extends NativeI16Enum ? U + : T extends NativeU32Enum ? U + : T extends NativeI32Enum ? U + : number + : T extends NativeBigIntType ? bigint + : T extends NativeBooleanType ? boolean + : T extends NativePointerType + ? T extends NativeTypedPointer ? U | null + : PointerValue + : T extends NativeBufferType ? PointerValue + : T extends NativeFunctionType + ? T extends NativeTypedFunction ? PointerObject | null + : PointerValue + : never; -/** @category Web APIs */ -declare type URLPatternInput = string | URLPatternInit; + /** Type conversion for foreign symbol return types. + * + * @category FFI + */ + export type FromNativeResultType< + T extends NativeResultType = NativeResultType, + > = T extends NativeStructType ? Uint8Array + : T extends NativeNumberType ? T extends NativeU8Enum ? U + : T extends NativeI8Enum ? U + : T extends NativeU16Enum ? U + : T extends NativeI16Enum ? U + : T extends NativeU32Enum ? U + : T extends NativeI32Enum ? U + : number + : T extends NativeBigIntType ? bigint + : T extends NativeBooleanType ? boolean + : T extends NativePointerType + ? T extends NativeTypedPointer ? U | null + : PointerValue + : T extends NativeBufferType ? PointerValue + : T extends NativeFunctionType + ? T extends NativeTypedFunction ? PointerObject | null + : PointerValue + : T extends NativeVoidType ? void + : never; -/** @category Web APIs */ -declare interface URLPatternComponentResult { - input: string; - groups: Record; -} + /** @category FFI + */ + export type FromNativeParameterTypes = + // + [T[number][]] extends [T] ? FromNativeType[] + : [readonly T[number][]] extends [T] + ? readonly FromNativeType[] + : T extends readonly [...NativeType[]] ? { + [K in keyof T]: FromNativeType; + } + : never; -/** `URLPatternResult` is the object returned from `URLPattern.exec`. - * - * @category Web APIs - */ -declare interface URLPatternResult { - /** The inputs provided when matching. */ - inputs: [URLPatternInit] | [URLPatternInit, string]; + /** The interface for a foreign function as defined by its parameter and result + * types. + * + * @category FFI + */ + export interface ForeignFunction< + Parameters extends readonly NativeType[] = readonly NativeType[], + Result extends NativeResultType = NativeResultType, + NonBlocking extends boolean = boolean, + > { + /** Name of the symbol. + * + * Defaults to the key name in symbols object. */ + name?: string; + /** The parameters of the foreign function. */ + parameters: Parameters; + /** The result (return value) of the foreign function. */ + result: Result; + /** When `true`, function calls will run on a dedicated blocking thread and + * will return a `Promise` resolving to the `result`. */ + nonblocking?: NonBlocking; + /** When `true`, dlopen will not fail if the symbol is not found. + * Instead, the symbol will be set to `null`. + * + * @default {false} */ + optional?: boolean; + } - /** The matched result for the `protocol` matcher. */ - protocol: URLPatternComponentResult; - /** The matched result for the `username` matcher. */ - username: URLPatternComponentResult; - /** The matched result for the `password` matcher. */ - password: URLPatternComponentResult; - /** The matched result for the `hostname` matcher. */ - hostname: URLPatternComponentResult; - /** The matched result for the `port` matcher. */ - port: URLPatternComponentResult; - /** The matched result for the `pathname` matcher. */ - pathname: URLPatternComponentResult; - /** The matched result for the `search` matcher. */ - search: URLPatternComponentResult; - /** The matched result for the `hash` matcher. */ - hash: URLPatternComponentResult; -} + /** @category FFI + */ + export interface ForeignStatic { + /** Name of the symbol, defaults to the key name in symbols object. */ + name?: string; + /** The type of the foreign static value. */ + type: Type; + /** When `true`, dlopen will not fail if the symbol is not found. + * Instead, the symbol will be set to `null`. + * + * @default {false} */ + optional?: boolean; + } -/** - * The URLPattern API provides a web platform primitive for matching URLs based - * on a convenient pattern syntax. - * - * The syntax is based on path-to-regexp. Wildcards, named capture groups, - * regular groups, and group modifiers are all supported. - * - * ```ts - * // Specify the pattern as structured data. - * const pattern = new URLPattern({ pathname: "/users/:user" }); - * const match = pattern.exec("https://blog.example.com/users/joe"); - * console.log(match.pathname.groups.user); // joe - * ``` - * - * ```ts - * // Specify a fully qualified string pattern. - * const pattern = new URLPattern("https://example.com/books/:id"); - * console.log(pattern.test("https://example.com/books/123")); // true - * console.log(pattern.test("https://deno.land/books/123")); // false - * ``` - * - * ```ts - * // Specify a relative string pattern with a base URL. - * const pattern = new URLPattern("/article/:id", "https://blog.example.com"); - * console.log(pattern.test("https://blog.example.com/article")); // false - * console.log(pattern.test("https://blog.example.com/article/123")); // true - * ``` - * - * @category Web APIs - */ -declare interface URLPattern { - /** - * Test if the given input matches the stored pattern. - * - * The input can either be provided as an absolute URL string with an optional base, - * relative URL string with a required base, or as individual components - * in the form of an `URLPatternInit` object. - * - * ```ts - * const pattern = new URLPattern("https://example.com/books/:id"); + /** A foreign library interface descriptor. * - * // Test an absolute url string. - * console.log(pattern.test("https://example.com/books/123")); // true + * @category FFI + */ + export interface ForeignLibraryInterface { + [name: string]: ForeignFunction | ForeignStatic; + } + + /** A utility type that infers a foreign symbol. * - * // Test a relative url with a base. - * console.log(pattern.test("/books/123", "https://example.com")); // true + * @category FFI + */ + export type StaticForeignSymbol = + T extends ForeignFunction ? FromForeignFunction + : T extends ForeignStatic ? FromNativeType + : never; + + /** @category FFI + */ + export type FromForeignFunction = + T["parameters"] extends readonly [] ? () => StaticForeignSymbolReturnType + : ( + ...args: ToNativeParameterTypes + ) => StaticForeignSymbolReturnType; + + /** @category FFI + */ + export type StaticForeignSymbolReturnType = + ConditionalAsync>; + + /** @category FFI + */ + export type ConditionalAsync< + IsAsync extends boolean | undefined, + T, + > = IsAsync extends true ? Promise : T; + + /** A utility type that infers a foreign library interface. * - * // Test an object of url components. - * console.log(pattern.test({ pathname: "/books/123" })); // true - * ``` + * @category FFI */ - test(input: URLPatternInput, baseURL?: string): boolean; + export type StaticForeignLibraryInterface = + { + [K in keyof T]: T[K]["optional"] extends true + ? StaticForeignSymbol | null + : StaticForeignSymbol; + }; - /** - * Match the given input against the stored pattern. + /** A non-null pointer, represented as an object + * at runtime. The object's prototype is `null` + * and cannot be changed. The object cannot be + * assigned to either and is thus entirely read-only. * - * The input can either be provided as an absolute URL string with an optional base, - * relative URL string with a required base, or as individual components - * in the form of an `URLPatternInit` object. + * To interact with memory through a pointer use the + * {@linkcode UnsafePointerView} class. To create a + * pointer from an address or the get the address of + * a pointer use the static methods of the + * {@linkcode UnsafePointer} class. * - * ```ts - * const pattern = new URLPattern("https://example.com/books/:id"); + * @category FFI + */ + export interface PointerObject { + [brand]: T; + } + + /** Pointers are represented either with a {@linkcode PointerObject} + * object or a `null` if the pointer is null. * - * // Match an absolute url string. - * let match = pattern.exec("https://example.com/books/123"); - * console.log(match.pathname.groups.id); // 123 + * @category FFI + */ + export type PointerValue = null | PointerObject; + + /** A collection of static functions for interacting with pointer objects. * - * // Match a relative url with a base. - * match = pattern.exec("/books/123", "https://example.com"); - * console.log(match.pathname.groups.id); // 123 + * @category FFI + */ + export class UnsafePointer { + /** Create a pointer from a numeric value. This one is really dangerous! */ + static create(value: bigint): PointerValue; + /** Returns `true` if the two pointers point to the same address. */ + static equals(a: PointerValue, b: PointerValue): boolean; + /** Return the direct memory pointer to the typed array in memory. */ + static of( + value: Deno.UnsafeCallback | BufferSource, + ): PointerValue; + /** Return a new pointer offset from the original by `offset` bytes. */ + static offset( + value: PointerObject, + offset: number, + ): PointerValue; + /** Get the numeric value of a pointer */ + static value(value: PointerValue): bigint; + } + + /** An unsafe pointer view to a memory location as specified by the `pointer` + * value. The `UnsafePointerView` API follows the standard built in interface + * {@linkcode DataView} for accessing the underlying types at an memory + * location (numbers, strings and raw bytes). * - * // Match an object of url components. - * match = pattern.exec({ pathname: "/books/123" }); - * console.log(match.pathname.groups.id); // 123 - * ``` + * @category FFI */ - exec(input: URLPatternInput, baseURL?: string): URLPatternResult | null; + export class UnsafePointerView { + constructor(pointer: PointerObject); - /** The pattern string for the `protocol`. */ - readonly protocol: string; - /** The pattern string for the `username`. */ - readonly username: string; - /** The pattern string for the `password`. */ - readonly password: string; - /** The pattern string for the `hostname`. */ - readonly hostname: string; - /** The pattern string for the `port`. */ - readonly port: string; - /** The pattern string for the `pathname`. */ - readonly pathname: string; - /** The pattern string for the `search`. */ - readonly search: string; - /** The pattern string for the `hash`. */ - readonly hash: string; -} + pointer: PointerObject; -/** - * The URLPattern API provides a web platform primitive for matching URLs based - * on a convenient pattern syntax. - * - * The syntax is based on path-to-regexp. Wildcards, named capture groups, - * regular groups, and group modifiers are all supported. - * - * ```ts - * // Specify the pattern as structured data. - * const pattern = new URLPattern({ pathname: "/users/:user" }); - * const match = pattern.exec("https://blog.example.com/users/joe"); - * console.log(match.pathname.groups.user); // joe - * ``` - * - * ```ts - * // Specify a fully qualified string pattern. - * const pattern = new URLPattern("https://example.com/books/:id"); - * console.log(pattern.test("https://example.com/books/123")); // true - * console.log(pattern.test("https://deno.land/books/123")); // false - * ``` - * - * ```ts - * // Specify a relative string pattern with a base URL. - * const pattern = new URLPattern("/article/:id", "https://blog.example.com"); - * console.log(pattern.test("https://blog.example.com/article")); // false - * console.log(pattern.test("https://blog.example.com/article/123")); // true - * ``` - * - * @category Web APIs - */ -declare var URLPattern: { - readonly prototype: URLPattern; - new (input: URLPatternInput, baseURL?: string): URLPattern; -}; + /** Gets a boolean at the specified byte offset from the pointer. */ + getBool(offset?: number): boolean; + /** Gets an unsigned 8-bit integer at the specified byte offset from the + * pointer. */ + getUint8(offset?: number): number; + /** Gets a signed 8-bit integer at the specified byte offset from the + * pointer. */ + getInt8(offset?: number): number; + /** Gets an unsigned 16-bit integer at the specified byte offset from the + * pointer. */ + getUint16(offset?: number): number; + /** Gets a signed 16-bit integer at the specified byte offset from the + * pointer. */ + getInt16(offset?: number): number; + /** Gets an unsigned 32-bit integer at the specified byte offset from the + * pointer. */ + getUint32(offset?: number): number; + /** Gets a signed 32-bit integer at the specified byte offset from the + * pointer. */ + getInt32(offset?: number): number; + /** Gets an unsigned 64-bit integer at the specified byte offset from the + * pointer. */ + getBigUint64(offset?: number): bigint; + /** Gets a signed 64-bit integer at the specified byte offset from the + * pointer. */ + getBigInt64(offset?: number): bigint; + /** Gets a signed 32-bit float at the specified byte offset from the + * pointer. */ + getFloat32(offset?: number): number; + /** Gets a signed 64-bit float at the specified byte offset from the + * pointer. */ + getFloat64(offset?: number): number; + /** Gets a pointer at the specified byte offset from the pointer */ + getPointer(offset?: number): PointerValue; + /** Gets a C string (`null` terminated string) at the specified byte offset + * from the pointer. */ + getCString(offset?: number): string; + /** Gets a C string (`null` terminated string) at the specified byte offset + * from the specified pointer. */ + static getCString(pointer: PointerObject, offset?: number): string; + /** Gets an `ArrayBuffer` of length `byteLength` at the specified byte + * offset from the pointer. */ + getArrayBuffer(byteLength: number, offset?: number): ArrayBuffer; + /** Gets an `ArrayBuffer` of length `byteLength` at the specified byte + * offset from the specified pointer. */ + static getArrayBuffer( + pointer: PointerObject, + byteLength: number, + offset?: number, + ): ArrayBuffer; + /** Copies the memory of the pointer into a typed array. + * + * Length is determined from the typed array's `byteLength`. + * + * Also takes optional byte offset from the pointer. */ + copyInto(destination: BufferSource, offset?: number): void; + /** Copies the memory of the specified pointer into a typed array. + * + * Length is determined from the typed array's `byteLength`. + * + * Also takes optional byte offset from the pointer. */ + static copyInto( + pointer: PointerObject, + destination: BufferSource, + offset?: number, + ): void; + } -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + /** An unsafe pointer to a function, for calling functions that are not present + * as symbols. + * + * @category FFI + */ + export class UnsafeFnPointer { + /** The pointer to the function. */ + pointer: PointerObject; + /** The definition of the function. */ + definition: Fn; -// deno-lint-ignore-file no-explicit-any no-var + constructor( + pointer: PointerObject>>, + definition: Fn, + ); -/// -/// + /** Call the foreign function. */ + call: FromForeignFunction; + } -/** @category Web APIs */ -declare interface DOMException extends Error { + /** Definition of a unsafe callback function. + * + * @category FFI + */ + export interface UnsafeCallbackDefinition< + Parameters extends readonly NativeType[] = readonly NativeType[], + Result extends NativeResultType = NativeResultType, + > { + /** The parameters of the callbacks. */ + parameters: Parameters; + /** The current result of the callback. */ + result: Result; + } + + /** An unsafe callback function. + * + * @category FFI + */ + export type UnsafeCallbackFunction< + Parameters extends readonly NativeType[] = readonly NativeType[], + Result extends NativeResultType = NativeResultType, + > = Parameters extends readonly [] ? () => ToNativeResultType + : ( + ...args: FromNativeParameterTypes + ) => ToNativeResultType; + + /** An unsafe function pointer for passing JavaScript functions as C function + * pointers to foreign function calls. + * + * The function pointer remains valid until the `close()` method is called. + * + * All `UnsafeCallback` are always thread safe in that they can be called from + * foreign threads without crashing. However, they do not wake up the Deno event + * loop by default. + * + * If a callback is to be called from foreign threads, use the `threadSafe()` + * static constructor or explicitly call `ref()` to have the callback wake up + * the Deno event loop when called from foreign threads. This also stops + * Deno's process from exiting while the callback still exists and is not + * unref'ed. + * + * Use `deref()` to then allow Deno's process to exit. Calling `deref()` on + * a ref'ed callback does not stop it from waking up the Deno event loop when + * called from foreign threads. + * + * @category FFI + */ + export class UnsafeCallback< + const Definition extends UnsafeCallbackDefinition = + UnsafeCallbackDefinition, + > { + constructor( + definition: Definition, + callback: UnsafeCallbackFunction< + Definition["parameters"], + Definition["result"] + >, + ); + + /** The pointer to the unsafe callback. */ + readonly pointer: PointerObject; + /** The definition of the unsafe callback. */ + readonly definition: Definition; + /** The callback function. */ + readonly callback: UnsafeCallbackFunction< + Definition["parameters"], + Definition["result"] + >; + + /** + * Creates an {@linkcode UnsafeCallback} and calls `ref()` once to allow it to + * wake up the Deno event loop when called from foreign threads. + * + * This also stops Deno's process from exiting while the callback still + * exists and is not unref'ed. + */ + static threadSafe< + Definition extends UnsafeCallbackDefinition = UnsafeCallbackDefinition, + >( + definition: Definition, + callback: UnsafeCallbackFunction< + Definition["parameters"], + Definition["result"] + >, + ): UnsafeCallback; + + /** + * Increments the callback's reference counting and returns the new + * reference count. + * + * After `ref()` has been called, the callback always wakes up the + * Deno event loop when called from foreign threads. + * + * If the callback's reference count is non-zero, it keeps Deno's + * process from exiting. + */ + ref(): number; + + /** + * Decrements the callback's reference counting and returns the new + * reference count. + * + * Calling `unref()` does not stop a callback from waking up the Deno + * event loop when called from foreign threads. + * + * If the callback's reference counter is zero, it no longer keeps + * Deno's process from exiting. + */ + unref(): number; + + /** + * Removes the C function pointer associated with this instance. + * + * Continuing to use the instance or the C function pointer after closing + * the `UnsafeCallback` will lead to errors and crashes. + * + * Calling this method sets the callback's reference counting to zero, + * stops the callback from waking up the Deno event loop when called from + * foreign threads and no longer keeps Deno's process from exiting. + */ + close(): void; + } + + /** A dynamic library resource. Use {@linkcode Deno.dlopen} to load a dynamic + * library and return this interface. + * + * @category FFI + */ + export interface DynamicLibrary { + /** All of the registered library along with functions for calling them. */ + symbols: StaticForeignLibraryInterface; + /** Removes the pointers associated with the library symbols. + * + * Continuing to use symbols that are part of the library will lead to + * errors and crashes. + * + * Calling this method will also immediately set any references to zero and + * will no longer keep Deno's process from exiting. + */ + close(): void; + } + + /** Opens an external dynamic library and registers symbols, making foreign + * functions available to be called. + * + * Requires `allow-ffi` permission. Loading foreign dynamic libraries can in + * theory bypass all of the sandbox permissions. While it is a separate + * permission users should acknowledge in practice that is effectively the + * same as running with the `allow-all` permission. + * + * @example Given a C library which exports a foreign function named `add()` + * + * ```ts + * // Determine library extension based on + * // your OS. + * let libSuffix = ""; + * switch (Deno.build.os) { + * case "windows": + * libSuffix = "dll"; + * break; + * case "darwin": + * libSuffix = "dylib"; + * break; + * default: + * libSuffix = "so"; + * break; + * } + * + * const libName = `./libadd.${libSuffix}`; + * // Open library and define exported symbols + * const dylib = Deno.dlopen( + * libName, + * { + * "add": { parameters: ["isize", "isize"], result: "isize" }, + * } as const, + * ); + * + * // Call the symbol `add` + * const result = dylib.symbols.add(35n, 34n); // 69n + * + * console.log(`Result from external addition of 35 and 34: ${result}`); + * ``` + * + * @tags allow-ffi + * @category FFI + */ + export function dlopen( + filename: string | URL, + symbols: S, + ): DynamicLibrary; + + /** + * A custom `HttpClient` for use with {@linkcode fetch} function. This is + * designed to allow custom certificates or proxies to be used with `fetch()`. + * + * @example ```ts + * const caCert = await Deno.readTextFile("./ca.pem"); + * const client = Deno.createHttpClient({ caCerts: [ caCert ] }); + * const req = await fetch("https://myserver.com", { client }); + * ``` + * + * @category Fetch + */ + export class HttpClient implements Disposable { + /** Close the HTTP client. */ + close(): void; + + [Symbol.dispose](): void; + } + + /** + * The options used when creating a {@linkcode Deno.HttpClient}. + * + * @category Fetch + */ + export interface CreateHttpClientOptions { + /** A list of root certificates that will be used in addition to the + * default root certificates to verify the peer's certificate. + * + * Must be in PEM format. */ + caCerts?: string[]; + /** A HTTP proxy to use for new connections. */ + proxy?: Proxy; + /** Sets the maximum number of idle connections per host allowed in the pool. */ + poolMaxIdlePerHost?: number; + /** Set an optional timeout for idle sockets being kept-alive. + * Set to false to disable the timeout. */ + poolIdleTimeout?: number | false; + /** + * Whether HTTP/1.1 is allowed or not. + * + * @default {true} + */ + http1?: boolean; + /** Whether HTTP/2 is allowed or not. + * + * @default {true} + */ + http2?: boolean; + /** Whether setting the host header is allowed or not. + * + * @default {false} + */ + allowHost?: boolean; + } + + /** + * The definition of a proxy when specifying + * {@linkcode Deno.CreateHttpClientOptions}. + * + * @category Fetch + */ + export interface Proxy { + /** The string URL of the proxy server to use. */ + url: string; + /** The basic auth credentials to be used against the proxy server. */ + basicAuth?: BasicAuth; + } + + /** + * Basic authentication credentials to be used with a {@linkcode Deno.Proxy} + * server when specifying {@linkcode Deno.CreateHttpClientOptions}. + * + * @category Fetch + */ + export interface BasicAuth { + /** The username to be used against the proxy server. */ + username: string; + /** The password to be used against the proxy server. */ + password: string; + } + + /** Create a custom HttpClient to use with {@linkcode fetch}. This is an + * extension of the web platform Fetch API which allows Deno to use custom + * TLS CA certificates and connect via a proxy while using `fetch()`. + * + * The `cert` and `key` options can be used to specify a client certificate + * and key to use when connecting to a server that requires client + * authentication (mutual TLS or mTLS). The `cert` and `key` options must be + * provided in PEM format. + * + * @example ```ts + * const caCert = await Deno.readTextFile("./ca.pem"); + * const client = Deno.createHttpClient({ caCerts: [ caCert ] }); + * const response = await fetch("https://myserver.com", { client }); + * ``` + * + * @example ```ts + * const client = Deno.createHttpClient({ + * proxy: { url: "http://myproxy.com:8080" } + * }); + * const response = await fetch("https://myserver.com", { client }); + * ``` + * + * @example ```ts + * const key = "----BEGIN PRIVATE KEY----..."; + * const cert = "----BEGIN CERTIFICATE----..."; + * const client = Deno.createHttpClient({ key, cert }); + * const response = await fetch("https://myserver.com", { client }); + * ``` + * + * @category Fetch + */ + export function createHttpClient( + options: + | CreateHttpClientOptions + | (CreateHttpClientOptions & TlsCertifiedKeyPem), + ): HttpClient; + + export {}; // only export exports +} + +// Copyright 2018-2025 the Deno authors. MIT license. + +// deno-lint-ignore-file no-explicit-any + +/// +/// + +/** @category I/O */ +interface Console { + assert(condition?: boolean, ...data: any[]): void; + clear(): void; + count(label?: string): void; + countReset(label?: string): void; + debug(...data: any[]): void; + dir(item?: any, options?: any): void; + dirxml(...data: any[]): void; + error(...data: any[]): void; + group(...data: any[]): void; + groupCollapsed(...data: any[]): void; + groupEnd(): void; + info(...data: any[]): void; + log(...data: any[]): void; + table(tabularData?: any, properties?: string[]): void; + time(label?: string): void; + timeEnd(label?: string): void; + timeLog(label?: string, ...data: any[]): void; + trace(...data: any[]): void; + warn(...data: any[]): void; + + /** This method is a noop, unless used in inspector */ + timeStamp(label?: string): void; + + /** This method is a noop, unless used in inspector */ + profile(label?: string): void; + + /** This method is a noop, unless used in inspector */ + profileEnd(label?: string): void; +} + +// Copyright 2018-2025 the Deno authors. MIT license. + +// deno-lint-ignore-file no-explicit-any no-var + +/// +/// + +/** @category URL */ +interface URLSearchParams { + /** Appends a specified key/value pair as a new search parameter. + * + * ```ts + * let searchParams = new URLSearchParams(); + * searchParams.append('name', 'first'); + * searchParams.append('name', 'second'); + * ``` + */ + append(name: string, value: string): void; + + /** Deletes search parameters that match a name, and optional value, + * from the list of all search parameters. + * + * ```ts + * let searchParams = new URLSearchParams([['name', 'value']]); + * searchParams.delete('name'); + * searchParams.delete('name', 'value'); + * ``` + */ + delete(name: string, value?: string): void; + + /** Returns all the values associated with a given search parameter + * as an array. + * + * ```ts + * searchParams.getAll('name'); + * ``` + */ + getAll(name: string): string[]; + + /** Returns the first value associated to the given search parameter. + * + * ```ts + * searchParams.get('name'); + * ``` + */ + get(name: string): string | null; + + /** Returns a boolean value indicating if a given parameter, + * or parameter and value pair, exists. + * + * ```ts + * searchParams.has('name'); + * searchParams.has('name', 'value'); + * ``` + */ + has(name: string, value?: string): boolean; + + /** Sets the value associated with a given search parameter to the + * given value. If there were several matching values, this method + * deletes the others. If the search parameter doesn't exist, this + * method creates it. + * + * ```ts + * searchParams.set('name', 'value'); + * ``` + */ + set(name: string, value: string): void; + + /** Sort all key/value pairs contained in this object in place and + * return undefined. The sort order is according to Unicode code + * points of the keys. + * + * ```ts + * searchParams.sort(); + * ``` + */ + sort(): void; + + /** Calls a function for each element contained in this object in + * place and return undefined. Optionally accepts an object to use + * as this when executing callback as second argument. + * + * ```ts + * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); + * params.forEach((value, key, parent) => { + * console.log(value, key, parent); + * }); + * ``` + */ + forEach( + callbackfn: (value: string, key: string, parent: this) => void, + thisArg?: any, + ): void; + + /** Returns an iterator allowing to go through all keys contained + * in this object. + * + * ```ts + * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); + * for (const key of params.keys()) { + * console.log(key); + * } + * ``` + */ + keys(): IterableIterator; + + /** Returns an iterator allowing to go through all values contained + * in this object. + * + * ```ts + * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); + * for (const value of params.values()) { + * console.log(value); + * } + * ``` + */ + values(): IterableIterator; + + /** Returns an iterator allowing to go through all key/value + * pairs contained in this object. + * + * ```ts + * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); + * for (const [key, value] of params.entries()) { + * console.log(key, value); + * } + * ``` + */ + entries(): IterableIterator<[string, string]>; + + /** Returns an iterator allowing to go through all key/value + * pairs contained in this object. + * + * ```ts + * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); + * for (const [key, value] of params) { + * console.log(key, value); + * } + * ``` + */ + [Symbol.iterator](): IterableIterator<[string, string]>; + + /** Returns a query string suitable for use in a URL. + * + * ```ts + * searchParams.toString(); + * ``` + */ + toString(): string; + + /** Contains the number of search parameters + * + * ```ts + * searchParams.size + * ``` + */ + size: number; +} + +/** @category URL */ +declare var URLSearchParams: { + readonly prototype: URLSearchParams; + new ( + init?: Iterable | Record | string, + ): URLSearchParams; +}; + +/** The URL interface represents an object providing static methods used for + * creating object URLs. + * + * @category URL + */ +interface URL { + hash: string; + host: string; + hostname: string; + href: string; + toString(): string; + readonly origin: string; + password: string; + pathname: string; + port: string; + protocol: string; + search: string; + readonly searchParams: URLSearchParams; + username: string; + toJSON(): string; +} + +/** The URL interface represents an object providing static methods used for + * creating object URLs. + * + * @category URL + */ +declare var URL: { + readonly prototype: URL; + new (url: string | URL, base?: string | URL): URL; + parse(url: string | URL, base?: string | URL): URL | null; + canParse(url: string | URL, base?: string | URL): boolean; + createObjectURL(blob: Blob): string; + revokeObjectURL(url: string): void; +}; + +/** @category URL */ +interface URLPatternInit { + protocol?: string; + username?: string; + password?: string; + hostname?: string; + port?: string; + pathname?: string; + search?: string; + hash?: string; + baseURL?: string; +} + +/** @category URL */ +type URLPatternInput = string | URLPatternInit; + +/** @category URL */ +interface URLPatternComponentResult { + input: string; + groups: Record; +} + +/** `URLPatternResult` is the object returned from `URLPattern.exec`. + * + * @category URL + */ +interface URLPatternResult { + /** The inputs provided when matching. */ + inputs: [URLPatternInit] | [URLPatternInit, string]; + + /** The matched result for the `protocol` matcher. */ + protocol: URLPatternComponentResult; + /** The matched result for the `username` matcher. */ + username: URLPatternComponentResult; + /** The matched result for the `password` matcher. */ + password: URLPatternComponentResult; + /** The matched result for the `hostname` matcher. */ + hostname: URLPatternComponentResult; + /** The matched result for the `port` matcher. */ + port: URLPatternComponentResult; + /** The matched result for the `pathname` matcher. */ + pathname: URLPatternComponentResult; + /** The matched result for the `search` matcher. */ + search: URLPatternComponentResult; + /** The matched result for the `hash` matcher. */ + hash: URLPatternComponentResult; +} + +/** + * Options for the {@linkcode URLPattern} constructor. + * + * @category URL + */ +interface URLPatternOptions { + /** + * Enables case-insensitive matching. + * + * @default {false} + */ + ignoreCase: boolean; +} + +/** + * The URLPattern API provides a web platform primitive for matching URLs based + * on a convenient pattern syntax. + * + * The syntax is based on path-to-regexp. Wildcards, named capture groups, + * regular groups, and group modifiers are all supported. + * + * ```ts + * // Specify the pattern as structured data. + * const pattern = new URLPattern({ pathname: "/users/:user" }); + * const match = pattern.exec("https://blog.example.com/users/joe"); + * console.log(match.pathname.groups.user); // joe + * ``` + * + * ```ts + * // Specify a fully qualified string pattern. + * const pattern = new URLPattern("https://example.com/books/:id"); + * console.log(pattern.test("https://example.com/books/123")); // true + * console.log(pattern.test("https://deno.land/books/123")); // false + * ``` + * + * ```ts + * // Specify a relative string pattern with a base URL. + * const pattern = new URLPattern("/article/:id", "https://blog.example.com"); + * console.log(pattern.test("https://blog.example.com/article")); // false + * console.log(pattern.test("https://blog.example.com/article/123")); // true + * ``` + * + * @category URL + */ +interface URLPattern { + /** + * Test if the given input matches the stored pattern. + * + * The input can either be provided as an absolute URL string with an optional base, + * relative URL string with a required base, or as individual components + * in the form of an `URLPatternInit` object. + * + * ```ts + * const pattern = new URLPattern("https://example.com/books/:id"); + * + * // Test an absolute url string. + * console.log(pattern.test("https://example.com/books/123")); // true + * + * // Test a relative url with a base. + * console.log(pattern.test("/books/123", "https://example.com")); // true + * + * // Test an object of url components. + * console.log(pattern.test({ pathname: "/books/123" })); // true + * ``` + */ + test(input: URLPatternInput, baseURL?: string): boolean; + + /** + * Match the given input against the stored pattern. + * + * The input can either be provided as an absolute URL string with an optional base, + * relative URL string with a required base, or as individual components + * in the form of an `URLPatternInit` object. + * + * ```ts + * const pattern = new URLPattern("https://example.com/books/:id"); + * + * // Match an absolute url string. + * let match = pattern.exec("https://example.com/books/123"); + * console.log(match.pathname.groups.id); // 123 + * + * // Match a relative url with a base. + * match = pattern.exec("/books/123", "https://example.com"); + * console.log(match.pathname.groups.id); // 123 + * + * // Match an object of url components. + * match = pattern.exec({ pathname: "/books/123" }); + * console.log(match.pathname.groups.id); // 123 + * ``` + */ + exec(input: URLPatternInput, baseURL?: string): URLPatternResult | null; + + /** The pattern string for the `protocol`. */ + readonly protocol: string; + /** The pattern string for the `username`. */ + readonly username: string; + /** The pattern string for the `password`. */ + readonly password: string; + /** The pattern string for the `hostname`. */ + readonly hostname: string; + /** The pattern string for the `port`. */ + readonly port: string; + /** The pattern string for the `pathname`. */ + readonly pathname: string; + /** The pattern string for the `search`. */ + readonly search: string; + /** The pattern string for the `hash`. */ + readonly hash: string; + + /** Whether or not any of the specified groups use regexp groups. */ + readonly hasRegExpGroups: boolean; +} + +/** + * The URLPattern API provides a web platform primitive for matching URLs based + * on a convenient pattern syntax. + * + * The syntax is based on path-to-regexp. Wildcards, named capture groups, + * regular groups, and group modifiers are all supported. + * + * ```ts + * // Specify the pattern as structured data. + * const pattern = new URLPattern({ pathname: "/users/:user" }); + * const match = pattern.exec("https://blog.example.com/users/joe"); + * console.log(match.pathname.groups.user); // joe + * ``` + * + * ```ts + * // Specify a fully qualified string pattern. + * const pattern = new URLPattern("https://example.com/books/:id"); + * console.log(pattern.test("https://example.com/books/123")); // true + * console.log(pattern.test("https://deno.land/books/123")); // false + * ``` + * + * ```ts + * // Specify a relative string pattern with a base URL. + * const pattern = new URLPattern("/article/:id", "https://blog.example.com"); + * console.log(pattern.test("https://blog.example.com/article")); // false + * console.log(pattern.test("https://blog.example.com/article/123")); // true + * ``` + * + * @category URL + */ +declare var URLPattern: { + readonly prototype: URLPattern; + new ( + input: URLPatternInput, + baseURL: string, + options?: URLPatternOptions, + ): URLPattern; + new (input?: URLPatternInput, options?: URLPatternOptions): URLPattern; +}; + +// Copyright 2018-2025 the Deno authors. MIT license. + +// deno-lint-ignore-file no-explicit-any no-var + +/// +/// + +/** @category Platform */ +interface DOMException extends Error { readonly name: string; readonly message: string; + /** @deprecated */ readonly code: number; readonly INDEX_SIZE_ERR: 1; readonly DOMSTRING_SIZE_ERR: 2; @@ -6667,7 +6645,7 @@ declare interface DOMException extends Error { readonly DATA_CLONE_ERR: 25; } -/** @category Web APIs */ +/** @category Platform */ declare var DOMException: { readonly prototype: DOMException; new (message?: string, name?: string): DOMException; @@ -6698,8 +6676,8 @@ declare var DOMException: { readonly DATA_CLONE_ERR: 25; }; -/** @category DOM Events */ -declare interface EventInit { +/** @category Events */ +interface EventInit { bubbles?: boolean; cancelable?: boolean; composed?: boolean; @@ -6707,13 +6685,14 @@ declare interface EventInit { /** An event which takes place in the DOM. * - * @category DOM Events + * @category Events */ -declare interface Event { +interface Event { /** Returns true or false depending on how event was initialized. True if * event goes through its target's ancestors in reverse tree order, and * false otherwise. */ readonly bubbles: boolean; + /** @deprecated */ cancelBubble: boolean; /** Returns true or false depending on how event was initialized. Its return * value does not always carry meaning, but true can indicate that part of the @@ -6736,6 +6715,10 @@ declare interface Event { /** Returns true if event was dispatched by the user agent, and false * otherwise. */ readonly isTrusted: boolean; + /** @deprecated */ + returnValue: boolean; + /** @deprecated */ + readonly srcElement: EventTarget | null; /** Returns the object to which event is dispatched (its target). */ readonly target: EventTarget | null; /** Returns the event's timestamp as the number of milliseconds measured @@ -6748,6 +6731,8 @@ declare interface Event { * the shadow root's mode is "closed" that are not reachable from event's * currentTarget. */ composedPath(): EventTarget[]; + /** @deprecated */ + initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void; /** If invoked when the cancelable attribute value is true, and while * executing a listener for the event with passive set to false, signals to * the operation that caused event to be dispatched that it needs to be @@ -6760,32 +6745,32 @@ declare interface Event { /** When dispatched in a tree, invoking this method prevents event from * reaching any objects other than the current object. */ stopPropagation(): void; - readonly AT_TARGET: number; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - readonly NONE: number; + readonly NONE: 0; + readonly CAPTURING_PHASE: 1; + readonly AT_TARGET: 2; + readonly BUBBLING_PHASE: 3; } /** An event which takes place in the DOM. * - * @category DOM Events + * @category Events */ declare var Event: { readonly prototype: Event; new (type: string, eventInitDict?: EventInit): Event; - readonly AT_TARGET: number; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - readonly NONE: number; + readonly NONE: 0; + readonly CAPTURING_PHASE: 1; + readonly AT_TARGET: 2; + readonly BUBBLING_PHASE: 3; }; /** * EventTarget is a DOM interface implemented by objects that can receive events * and may have listeners for them. * - * @category DOM Events + * @category Events */ -declare interface EventTarget { +interface EventTarget { /** Appends an event listener for events whose type attribute value is type. * The callback argument sets the callback that will be invoked when the event * is dispatched. @@ -6831,42 +6816,42 @@ declare interface EventTarget { * EventTarget is a DOM interface implemented by objects that can receive events * and may have listeners for them. * - * @category DOM Events + * @category Events */ declare var EventTarget: { readonly prototype: EventTarget; new (): EventTarget; }; -/** @category DOM Events */ -declare interface EventListener { - (evt: Event): void | Promise; +/** @category Events */ +interface EventListener { + (evt: Event): void; } -/** @category DOM Events */ -declare interface EventListenerObject { - handleEvent(evt: Event): void | Promise; +/** @category Events */ +interface EventListenerObject { + handleEvent(evt: Event): void; } -/** @category DOM Events */ -declare type EventListenerOrEventListenerObject = +/** @category Events */ +type EventListenerOrEventListenerObject = | EventListener | EventListenerObject; -/** @category DOM Events */ -declare interface AddEventListenerOptions extends EventListenerOptions { +/** @category Events */ +interface AddEventListenerOptions extends EventListenerOptions { once?: boolean; passive?: boolean; signal?: AbortSignal; } -/** @category DOM Events */ -declare interface EventListenerOptions { +/** @category Events */ +interface EventListenerOptions { capture?: boolean; } -/** @category DOM Events */ -declare interface ProgressEventInit extends EventInit { +/** @category Events */ +interface ProgressEventInit extends EventInit { lengthComputable?: boolean; loaded?: number; total?: number; @@ -6876,10 +6861,9 @@ declare interface ProgressEventInit extends EventInit { * (for an XMLHttpRequest, or the loading of the underlying resource of an * ,