From 680607c460f9dfae8cd0c5f1d496274d4460ca1b Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:32:17 +0100 Subject: [PATCH 01/32] chore: Add `bump_otel_instrumentations` cursor command (#18253) Bumping OpenTelemetry instrumentations is an important but tedious task, all instrumentations have to be bumped in lockstep across the codebase. That includes easy to miss dev-packages and third party instrumentations like prisma's. This command should make it easier to do that. Example of a PR that was kicked off with this command: https://github.com/getsentry/sentry-javascript/pull/18239 --- .../commands/bump_otel_instrumentations.md | 32 ++++++++++++++++++ ...upgrade_opentelemetry_instrumentations.mdc | 33 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 .cursor/commands/bump_otel_instrumentations.md create mode 100644 .cursor/rules/upgrade_opentelemetry_instrumentations.mdc diff --git a/.cursor/commands/bump_otel_instrumentations.md b/.cursor/commands/bump_otel_instrumentations.md new file mode 100644 index 000000000000..ff1e6cfcbcc8 --- /dev/null +++ b/.cursor/commands/bump_otel_instrumentations.md @@ -0,0 +1,32 @@ +# Bump OpenTelemetry instrumentations + +1. Ensure you're on the `develop` branch with the latest changes: + - If you have unsaved changes, stash them with `git stash -u`. + - If you're on a different branch than `develop`, check out the develop branch using `git checkout develop`. + - Pull the latest updates from the remote repository by running `git pull origin develop`. + +2. Create a new branch `bump-otel-{yyyy-mm-dd}`, e.g. `bump-otel-2025-03-03` + +3. Create a new empty commit with the commit message `feat(deps): Bump OpenTelemetry instrumentations` + +4. Push the branch and create a draft PR, note down the PR number as {PR_NUMBER} + +5. Create a changelog entry in `CHANGELOG.md` under + `- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott` with the following format: + `- feat(deps): Bump OpenTelemetry instrumentations ([#{PR_NUMBER}](https://github.com/getsentry/sentry-javascript/pull/{PR_NUMBER}))` + +6. Find the "Upgrade OpenTelemetry instrumentations" rule in `.cursor/rules/upgrade_opentelemetry_instrumentations` and + follow those complete instructions step by step. + - Create one commit per package in `packages/**` with the commit message + `Bump OpenTelemetry instrumentations for {SDK}`, e.g. `Bump OpenTelemetry instrumentation for @sentry/node` + + - For each OpenTelemetry dependency bump, record an entry in the changelog with the format indented under the main + entry created in step 5: `- Bump @opentelemetry/{instrumentation} from {previous_version} to {new_version}`, e.g. + `- Bump @opentelemetry/instrumentation from 0.204.0 to 0.207.0` **CRITICAL**: Avoid duplicated entries, e.g. if we + bump @opentelemetry/instrumentation in two packages, keep a single changelog entry. + +7. Regenerate the yarn lockfile and run `yarn yarn-deduplicate` + +8. Run `yarn fix` to fix all formatting issues + +9. Finally update the PR description to list all dependency bumps diff --git a/.cursor/rules/upgrade_opentelemetry_instrumentations.mdc b/.cursor/rules/upgrade_opentelemetry_instrumentations.mdc new file mode 100644 index 000000000000..b650ae1f5041 --- /dev/null +++ b/.cursor/rules/upgrade_opentelemetry_instrumentations.mdc @@ -0,0 +1,33 @@ +--- +description: Use this rule if you are looking to grade OpenTelemetry instrumentations for the Sentry JavaScript SDKs +globs: * +alwaysApply: false +--- + +# Upgrading OpenTelemetry instrumentations + +1. For every package in packages/\*\*: + - When upgrading dependencies for OpenTelemetry instrumentations we need to first upgrade `@opentelemetry/instrumentation` to the latest version. + **CRITICAL**: `@opentelemetry/instrumentation` MUST NOT include any breaking changes. + Read through the changelog of `@opentelemetry/instrumentation` to figure out if breaking changes are included and fail with the reason if it does include breaking changes. + You can find the changelog at `https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/CHANGELOG.md` + + - After successfully upgrading `@opentelemetry/instrumentation` upgrade all `@opentelemetry/instrumentation-{instrumentation}` packages, e.g. `@opentelemetry/instrumentation-pg` + **CRITICAL**: `@opentelemetry/instrumentation-{instrumentation}` MUST NOT include any breaking changes. + Read through the changelog of `@opentelemetry/instrumentation-{instrumentation}` to figure out if breaking changes are included and fail with the reason if it does including breaking changes. + You can find the changelogs at `https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/packages/instrumentation-{instrumentation}/CHANGELOG.md`. + + - Finally, upgrade third party instrumentations to their latest versions, these are currently: + - @prisma/instrumentation + + **CRITICAL**: Upgrades to third party instrumentations MUST NOT include breaking changes. + Read through the changelog of each third party instrumentation to figure out if breaking changes are included and fail with the reason if it does include breaking changes. + +2. For packages and apps in dev-packages/\*\*: + - If an app depends on `@opentelemetry/instrumentation` >= 0.200.x upgrade it to the latest version. + **CRITICAL**: `@opentelemetry/instrumentation` MUST NOT include any breaking changes. + + - If an app depends on `@opentelemetry/instrumentation-http` >= 0.200.x upgrade it to the latest version. + **CRITICAL**: `@opentelemetry/instrumentation-http` MUST NOT include any breaking changes. + +3. Generate a new yarn lock file. From bee20fb0eaacab58d81c995e1509bce6d46b23a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:11:41 +0100 Subject: [PATCH 02/32] ci(deps): bump actions/upload-artifact from 4 to 5 (#18075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
Release notes

Sourced from actions/upload-artifact's releases.

v5.0.0

What's Changed

BREAKING CHANGE: this update supports Node v24.x. This is not a breaking change per-se but we're treating it as such.

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v5.0.0

v4.6.2

What's Changed

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.2

v4.6.1

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.1

v4.6.0

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.0

v4.5.0

What's Changed

New Contributors

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jan Peer Stöcklmair --- .github/workflows/build.yml | 14 +++++++------- .github/workflows/flaky-test-detector.yml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46d6e7d4fac9..610dced85c1b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -181,7 +181,7 @@ jobs: run: yarn build - name: Upload build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: build-output path: ${{ env.CACHED_BUILD_PATHS }} @@ -386,7 +386,7 @@ jobs: run: yarn build:tarball - name: Archive artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ github.sha }} retention-days: 90 @@ -629,7 +629,7 @@ jobs: format(' --shard={0}/{1}', matrix.shard, matrix.shards) || '' }} - name: Upload Playwright Traces - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: failure() with: name: @@ -692,7 +692,7 @@ jobs: yarn test:loader - name: Upload Playwright Traces - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: failure() with: name: playwright-traces-job_browser_loader_tests-${{ matrix.bundle}} @@ -1005,7 +1005,7 @@ jobs: run: ${{ matrix.assert-command || 'pnpm test:assert' }} - name: Upload Playwright Traces - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: failure() with: name: playwright-traces-job_e2e_playwright_tests-${{ matrix.test-application}} @@ -1019,7 +1019,7 @@ jobs: node ./scripts/normalize-e2e-test-dump-transaction-events.js - name: Upload E2E Test Event Dumps - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: failure() with: name: E2E Test Dump (${{ matrix.label || matrix.test-application }}) @@ -1131,7 +1131,7 @@ jobs: node ./scripts/normalize-e2e-test-dump-transaction-events.js - name: Upload E2E Test Event Dumps - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: failure() with: name: E2E Test Dump (${{ matrix.label || matrix.test-application }}) diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index 5103f1f43a2d..21a851fb43d6 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -71,7 +71,7 @@ jobs: TEST_RUN_COUNT: 'AUTO' - name: Upload Playwright Traces - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: failure() && steps.test.outcome == 'failure' with: name: playwright-test-results From 81c5d56d644d9f003ff06d08c0a3fe985cde6562 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:12:01 +0100 Subject: [PATCH 03/32] ci(deps): bump actions/setup-node from 4 to 6 (#18077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
Release notes

Sourced from actions/setup-node's releases.

v6.0.0

What's Changed

Breaking Changes

Dependency Upgrades

Full Changelog: https://github.com/actions/setup-node/compare/v5...v6.0.0

v5.0.0

What's Changed

Breaking Changes

This update, introduces automatic caching when a valid packageManager field is present in your package.json. This aims to improve workflow performance and make dependency management more seamless. To disable this automatic caching, set package-manager-cache: false

steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
  with:
    package-manager-cache: false

Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. See Release Notes

Dependency Upgrades

New Contributors

Full Changelog: https://github.com/actions/setup-node/compare/v4...v5.0.0

v4.4.0

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=4&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jan Peer Stöcklmair --- .github/workflows/auto-release.yml | 2 +- .github/workflows/build.yml | 40 ++++++++++----------- .github/workflows/canary.yml | 4 +-- .github/workflows/clear-cache.yml | 2 +- .github/workflows/external-contributors.yml | 2 +- .github/workflows/flaky-test-detector.yml | 2 +- .github/workflows/release.yml | 2 +- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 0507fe879c27..e1fdd98549ae 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -42,7 +42,7 @@ jobs: echo "version=$version" >> $GITHUB_OUTPUT - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 610dced85c1b..881b5f4b6580 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -142,7 +142,7 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' @@ -242,7 +242,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -271,7 +271,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -300,7 +300,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -330,7 +330,7 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' @@ -352,7 +352,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -374,7 +374,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -415,7 +415,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -456,7 +456,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Set up Bun @@ -481,7 +481,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Set up Deno @@ -518,7 +518,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - name: Restore caches @@ -607,7 +607,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -671,7 +671,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -719,7 +719,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -757,7 +757,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - name: Restore caches @@ -793,7 +793,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -821,7 +821,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - name: Restore caches @@ -873,7 +873,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Restore caches @@ -941,7 +941,7 @@ jobs: with: version: 9.15.9 - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' - name: Set up Bun @@ -1071,7 +1071,7 @@ jobs: with: version: 9.15.9 - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' - name: Restore caches diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 29814ffea09c..57290080c8de 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -35,7 +35,7 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Check canary cache @@ -125,7 +125,7 @@ jobs: version: 9.15.9 - name: Set up Node if: matrix.test-application != 'angular-20' - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' diff --git a/.github/workflows/clear-cache.yml b/.github/workflows/clear-cache.yml index 97aeb53365e7..0f5f2241b34a 100644 --- a/.github/workflows/clear-cache.yml +++ b/.github/workflows/clear-cache.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' diff --git a/.github/workflows/external-contributors.yml b/.github/workflows/external-contributors.yml index 1735a89a5446..c085f9958452 100644 --- a/.github/workflows/external-contributors.yml +++ b/.github/workflows/external-contributors.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index 21a851fb43d6..a6ed22e04f6a 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -32,7 +32,7 @@ jobs: - name: Check out current branch uses: actions/checkout@v5 - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' cache: 'yarn' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 05c465036ce4..954e8d3e5e84 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: token: ${{ steps.token.outputs.token }} fetch-depth: 0 - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: 'package.json' - name: Prepare release From d710314881a19a84ca76c78c18752ef2d08a1728 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 19 Nov 2025 14:01:22 +0100 Subject: [PATCH 04/32] test(nextjs): Remove debug logs from e2e test (#18250) --- .../test-applications/nextjs-15/instrumentation-client.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation-client.ts index 0737d2043169..4870c64e7959 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation-client.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation-client.ts @@ -2,11 +2,10 @@ import * as Sentry from '@sentry/nextjs'; Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions - dsn: 'https://username@domain/123', + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, sendDefaultPii: true, - debug: true, }); export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; From 1e5af715f4ebb42748ba75e47c3aaaf15f0678df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:39:19 +0100 Subject: [PATCH 05/32] ci(deps): bump github/codeql-action from 3 to 4 (#18076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
Release notes

Sourced from github/codeql-action's releases.

v3.31.2

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.31.2 - 30 Oct 2025

No user facing changes.

See the full CHANGELOG.md for more information.

v3.31.1

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.31.1 - 30 Oct 2025

  • The add-snippets input has been removed from the analyze action. This input has been deprecated since CodeQL Action 3.26.4 in August 2024 when this removal was announced.

See the full CHANGELOG.md for more information.

v3.31.0

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.31.0 - 24 Oct 2025

  • Bump minimum CodeQL bundle version to 2.17.6. #3223
  • When SARIF files are uploaded by the analyze or upload-sarif actions, the CodeQL Action automatically performs post-processing steps to prepare the data for the upload. Previously, these post-processing steps were only performed before an upload took place. We are now changing this so that the post-processing steps will always be performed, even when the SARIF files are not uploaded. This does not change anything for the upload-sarif action. For analyze, this may affect Advanced Setup for CodeQL users who specify a value other than always for the upload input. #3222

See the full CHANGELOG.md for more information.

v3.30.9

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.30.9 - 17 Oct 2025

  • Update default CodeQL bundle version to 2.23.3. #3205
  • Experimental: A new setup-codeql action has been added which is similar to init, except it only installs the CodeQL CLI and does not initialize a database. Do not use this in production as it is part of an internal experiment and subject to change at any time. #3204

See the full CHANGELOG.md for more information.

v3.30.8

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

... (truncated)

Changelog

Sourced from github/codeql-action's changelog.

4.31.2 - 30 Oct 2025

No user facing changes.

4.31.1 - 30 Oct 2025

  • The add-snippets input has been removed from the analyze action. This input has been deprecated since CodeQL Action 3.26.4 in August 2024 when this removal was announced.

4.31.0 - 24 Oct 2025

  • Bump minimum CodeQL bundle version to 2.17.6. #3223
  • When SARIF files are uploaded by the analyze or upload-sarif actions, the CodeQL Action automatically performs post-processing steps to prepare the data for the upload. Previously, these post-processing steps were only performed before an upload took place. We are now changing this so that the post-processing steps will always be performed, even when the SARIF files are not uploaded. This does not change anything for the upload-sarif action. For analyze, this may affect Advanced Setup for CodeQL users who specify a value other than always for the upload input. #3222

4.30.9 - 17 Oct 2025

  • Update default CodeQL bundle version to 2.23.3. #3205
  • Experimental: A new setup-codeql action has been added which is similar to init, except it only installs the CodeQL CLI and does not initialize a database. Do not use this in production as it is part of an internal experiment and subject to change at any time. #3204

4.30.8 - 10 Oct 2025

No user facing changes.

4.30.7 - 06 Oct 2025

  • [v4+ only] The CodeQL Action now runs on Node.js v24. #3169

3.30.6 - 02 Oct 2025

  • Update default CodeQL bundle version to 2.23.2. #3168

3.30.5 - 26 Sep 2025

  • We fixed a bug that was introduced in 3.30.4 with upload-sarif which resulted in files without a .sarif extension not getting uploaded. #3160

3.30.4 - 25 Sep 2025

  • We have improved the CodeQL Action's ability to validate that the workflow it is used in does not use different versions of the CodeQL Action for different workflow steps. Mixing different versions of the CodeQL Action in the same workflow is unsupported and can lead to unpredictable results. A warning will now be emitted from the codeql-action/init step if different versions of the CodeQL Action are detected in the workflow file. Additionally, an error will now be thrown by the other CodeQL Action steps if they load a configuration file that was generated by a different version of the codeql-action/init step. #3099 and #3100
  • We added support for reducing the size of dependency caches for Java analyses, which will reduce cache usage and speed up workflows. This will be enabled automatically at a later time. #3107
  • You can now run the latest CodeQL nightly bundle by passing tools: nightly to the init action. In general, the nightly bundle is unstable and we only recommend running it when directed by GitHub staff. #3130
  • Update default CodeQL bundle version to 2.23.1. #3118

3.30.3 - 10 Sep 2025

No user facing changes.

3.30.2 - 09 Sep 2025

  • Fixed a bug which could cause language autodetection to fail. #3084
  • Experimental: The quality-queries input that was added in 3.29.2 as part of an internal experiment is now deprecated and will be removed in an upcoming version of the CodeQL Action. It has been superseded by a new analysis-kinds input, which is part of the same internal experiment. Do not use this in production as it is subject to change at any time. #3064

... (truncated)

Commits
  • 74c8748 Update analyze/action.yml
  • 34c50c1 Merge pull request #3251 from github/mbg/user-error/enablement
  • 4ae68af Warn if the add-snippets input is used
  • 52a7bd7 Check for 403 status
  • 194ba0e Make error message tests less brittle
  • 53acf0b Turn enablement errors into configuration errors
  • ac9aeee Merge pull request #3249 from github/henrymercer/api-logging
  • d49e837 Merge branch 'main' into henrymercer/api-logging
  • 3d988b2 Pass minimal copy of core
  • 8cc18ac Merge pull request #3250 from github/henrymercer/prefer-fs-delete
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jan Peer Stöcklmair --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8c042c5aa44f..6d6b67201d5e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: config-file: ./.github/codeql/codeql-config.yml queries: security-extended @@ -63,7 +63,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions @@ -77,4 +77,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 From 902a06e75a034c4aca6d4228d897d0f31d4d3f9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:43:18 +0100 Subject: [PATCH 06/32] ci(deps): Bump actions/create-github-app-token from 2.1.1 to 2.1.4 (#17825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2.1.1 to 2.1.4.
Release notes

Sourced from actions/create-github-app-token's releases.

v2.1.4

2.1.4 (2025-09-13)

Bug Fixes

  • deps: bump @​octokit/auth-app from 7.2.1 to 8.0.1 (#257) (bef1eaf)

v2.1.3

2.1.3 (2025-09-13)

Bug Fixes

  • deps: bump undici from 7.8.0 to 7.10.0 in the production-dependencies group (#254) (f3d5ec2)

v2.1.2

2.1.2 (2025-09-12)

Bug Fixes

  • deps: bump @​octokit/request from 9.2.3 to 10.0.2 (#256) (5d7307b)
Commits
  • 6701853 build(release): 2.1.4 [skip ci]
  • bef1eaf fix(deps): bump @​octokit/auth-app from 7.2.1 to 8.0.1 (#257)
  • 1526738 build(release): 2.1.3 [skip ci]
  • f3d5ec2 fix(deps): bump undici from 7.8.0 to 7.10.0 in the production-dependencies gr...
  • def152b build(release): 2.1.2 [skip ci]
  • 5d7307b fix(deps): bump @​octokit/request from 9.2.3 to 10.0.2 (#256)
  • 525760a build(deps): bump stefanzweifel/git-auto-commit-action from 5.2.0 to 6.0.1 (#...
  • 8ab05a8 Add beta branch support for releases (#282)
  • d00315e build(deps): bump actions/checkout from 4 to 5 (#279)
  • fcc6c28 build(deps-dev): bump dotenv from 16.5.0 to 17.2.1 (#269)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/create-github-app-token&package-manager=github_actions&previous-version=2.1.1&new-version=2.1.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- > [!NOTE] > Update actions/create-github-app-token from 2.1.1 to 2.1.4 in auto-release and release workflows. > > - **CI Workflows**: > - Bump `actions/create-github-app-token` to `v2.1.4` in `.github/workflows/auto-release.yml` and `.github/workflows/release.yml`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e8434a819664875d674d4bc8759b315ce79d32a8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). > **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jan Peer Stöcklmair --- .github/workflows/auto-release.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index e1fdd98549ae..cfaf6db8abef 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 954e8d3e5e84..a2cb3fcd9600 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From 013f0bec0f686bcaeb5d2be45313efea8044123a Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 19 Nov 2025 15:07:11 +0100 Subject: [PATCH 07/32] chore(dev-deps): Update some dev dependencies (#17816) Just bumping some transitive dev deps to fix security warnings. --- .github/dependency-review-config.yml | 2 + packages/react/package.json | 2 - packages/replay-internal/package.json | 3 +- yarn.lock | 734 +++++++++++++------------- 4 files changed, 377 insertions(+), 364 deletions(-) diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml index 1a8f76e430d1..8608d2381ace 100644 --- a/.github/dependency-review-config.yml +++ b/.github/dependency-review-config.yml @@ -9,3 +9,5 @@ allow-ghsas: - GHSA-v784-fjjh-f8r4 # Next.js Cache poisoning - We require a vulnerable version for E2E testing - GHSA-gp8f-8m3g-qvj9 + # devalue vulnerability - this is just used by nuxt & astro as transitive dependency + - GHSA-vj54-72f3-p5jv diff --git a/packages/react/package.json b/packages/react/package.json index c51a7e2d3ee8..afd5f5f25bdd 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -52,7 +52,6 @@ "@types/history-4": "npm:@types/history@4.7.8", "@types/history-5": "npm:@types/history@4.7.8", "@types/hoist-non-react-statics": "^3.3.5", - "@types/node-fetch": "^2.6.11", "@types/react": "17.0.3", "@types/react-router-4": "npm:@types/react-router@4.0.25", "@types/react-router-5": "npm:@types/react-router@5.1.20", @@ -60,7 +59,6 @@ "eslint-plugin-react-hooks": "^4.0.8", "history-4": "npm:history@4.6.0", "history-5": "npm:history@4.9.0", - "node-fetch": "^2.6.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-3": "npm:react-router@3.2.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index af121d6a74c0..f1678cea2a15 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -86,8 +86,7 @@ "@sentry-internal/rrweb-snapshot": "2.40.0", "fflate": "0.8.2", "jest-matcher-utils": "^29.0.0", - "jsdom-worker": "^0.3.0", - "node-fetch": "^2.6.7" + "jsdom-worker": "^0.3.0" }, "dependencies": { "@sentry-internal/browser-utils": "10.26.0", diff --git a/yarn.lock b/yarn.lock index 082e1a032283..c93b8a45e748 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3012,6 +3012,11 @@ dependencies: tslib "^2.4.0" +"@esbuild/aix-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" + integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== + "@esbuild/aix-ppc64@0.20.0": version "0.20.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" @@ -3032,6 +3037,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353" integrity sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ== +"@esbuild/aix-ppc64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz#ee6b7163a13528e099ecf562b972f2bcebe0aa97" + integrity sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw== + "@esbuild/aix-ppc64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz#830d6476cbbca0c005136af07303646b419f1162" @@ -3042,20 +3052,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz#4e0f91776c2b340e75558f60552195f6fad09f18" integrity sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA== -"@esbuild/aix-ppc64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz#164b19122e2ed54f85469df9dea98ddb01d5e79e" - integrity sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw== - "@esbuild/android-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== -"@esbuild/android-arm64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.4.tgz#74752a09301b8c6b9a415fbda9fb71406a62a7b7" - integrity sha512-mRsi2vJsk4Bx/AFsNBqOH2fqedxn5L/moT58xgg51DjX1la64Z3Npicut2VbhvDFO26qjWtPMsVxCd80YTFVeg== +"@esbuild/android-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" + integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== "@esbuild/android-arm64@0.20.0": version "0.20.0" @@ -3077,6 +3082,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz#58565291a1fe548638adb9c584237449e5e14018" integrity sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw== +"@esbuild/android-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz#115fc76631e82dd06811bfaf2db0d4979c16e2cb" + integrity sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg== + "@esbuild/android-arm64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz#d11d4fc299224e729e2190cacadbcc00e7a9fd67" @@ -3087,11 +3097,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz#bc766407f1718923f6b8079c8c61bf86ac3a6a4f" integrity sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg== -"@esbuild/android-arm64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz#8f539e7def848f764f6432598e51cc3820fde3a5" - integrity sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA== - "@esbuild/android-arm@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80" @@ -3102,10 +3107,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== -"@esbuild/android-arm@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.4.tgz#c27363e1e280e577d9b5c8fa7c7a3be2a8d79bf5" - integrity sha512-uBIbiYMeSsy2U0XQoOGVVcpIktjLMEKa7ryz2RLr7L/vTnANNEsPVAh4xOv7ondGz6ac1zVb0F8Jx20rQikffQ== +"@esbuild/android-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" + integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== "@esbuild/android-arm@0.20.0": version "0.20.0" @@ -3127,6 +3132,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.1.tgz#5eb8c652d4c82a2421e3395b808e6d9c42c862ee" integrity sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ== +"@esbuild/android-arm@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.10.tgz#8d5811912da77f615398611e5bbc1333fe321aa9" + integrity sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w== + "@esbuild/android-arm@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.4.tgz#5660bd25080553dd2a28438f2a401a29959bd9b1" @@ -3137,20 +3147,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz#4290d6d3407bae3883ad2cded1081a234473ce26" integrity sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA== -"@esbuild/android-arm@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.6.tgz#4ceb0f40113e9861169be83e2a670c260dd234ff" - integrity sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg== - "@esbuild/android-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== -"@esbuild/android-x64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.4.tgz#6c9ee03d1488973d928618100048b75b147e0426" - integrity sha512-4iPufZ1TMOD3oBlGFqHXBpa3KFT46aLl6Vy7gwed0ZSYgHaZ/mihbYb4t7Z9etjkC9Al3ZYIoOaHrU60gcMy7g== +"@esbuild/android-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" + integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== "@esbuild/android-x64@0.20.0": version "0.20.0" @@ -3172,6 +3177,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.1.tgz#ae19d665d2f06f0f48a6ac9a224b3f672e65d517" integrity sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg== +"@esbuild/android-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.10.tgz#e3e96516b2d50d74105bb92594c473e30ddc16b1" + integrity sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg== + "@esbuild/android-x64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.4.tgz#18ddde705bf984e8cd9efec54e199ac18bc7bee1" @@ -3182,20 +3192,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz#40c11d9cbca4f2406548c8a9895d321bc3b35eff" integrity sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw== -"@esbuild/android-x64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.6.tgz#ad4f280057622c25fe985c08999443a195dc63a8" - integrity sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A== - "@esbuild/darwin-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== -"@esbuild/darwin-arm64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.4.tgz#64e2ee945e5932cd49812caa80e8896e937e2f8b" - integrity sha512-Lviw8EzxsVQKpbS+rSt6/6zjn9ashUZ7Tbuvc2YENgRl0yZTktGlachZ9KMJUsVjZEGFVu336kl5lBgDN6PmpA== +"@esbuild/darwin-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" + integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== "@esbuild/darwin-arm64@0.20.0": version "0.20.0" @@ -3217,6 +3222,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz#05b17f91a87e557b468a9c75e9d85ab10c121b16" integrity sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q== +"@esbuild/darwin-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz#6af6bb1d05887dac515de1b162b59dc71212ed76" + integrity sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA== + "@esbuild/darwin-arm64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz#b0b7fb55db8fc6f5de5a0207ae986eb9c4766e67" @@ -3227,20 +3237,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz#49d8bf8b1df95f759ac81eb1d0736018006d7e34" integrity sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ== -"@esbuild/darwin-arm64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz#d1f04027396b3d6afc96bacd0d13167dfd9f01f7" - integrity sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA== - "@esbuild/darwin-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== -"@esbuild/darwin-x64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.4.tgz#d8e26e1b965df284692e4d1263ba69a49b39ac7a" - integrity sha512-YHbSFlLgDwglFn0lAO3Zsdrife9jcQXQhgRp77YiTDja23FrC2uwnhXMNkAucthsf+Psr7sTwYEryxz6FPAVqw== +"@esbuild/darwin-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" + integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== "@esbuild/darwin-x64@0.20.0": version "0.20.0" @@ -3262,6 +3267,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz#c58353b982f4e04f0d022284b8ba2733f5ff0931" integrity sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw== +"@esbuild/darwin-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz#99ae82347fbd336fc2d28ffd4f05694e6e5b723d" + integrity sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg== + "@esbuild/darwin-x64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz#e6813fdeba0bba356cb350a4b80543fbe66bf26f" @@ -3272,20 +3282,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz#e27a5d92a14886ef1d492fd50fc61a2d4d87e418" integrity sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ== -"@esbuild/darwin-x64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz#2b4a6cedb799f635758d7832d75b23772c8ef68f" - integrity sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg== - "@esbuild/freebsd-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== -"@esbuild/freebsd-arm64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.4.tgz#29751a41b242e0a456d89713b228f1da4f45582f" - integrity sha512-vz59ijyrTG22Hshaj620e5yhs2dU1WJy723ofc+KUgxVCM6zxQESmWdMuVmUzxtGqtj5heHyB44PjV/HKsEmuQ== +"@esbuild/freebsd-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" + integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== "@esbuild/freebsd-arm64@0.20.0": version "0.20.0" @@ -3307,6 +3312,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz#f9220dc65f80f03635e1ef96cfad5da1f446f3bc" integrity sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA== +"@esbuild/freebsd-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz#0c6d5558a6322b0bdb17f7025c19bd7d2359437d" + integrity sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg== + "@esbuild/freebsd-arm64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz#dc11a73d3ccdc308567b908b43c6698e850759be" @@ -3317,20 +3327,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz#97cede59d638840ca104e605cdb9f1b118ba0b1c" integrity sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw== -"@esbuild/freebsd-arm64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz#a26266cc97dd78dc3c3f3d6788b1b83697b1055d" - integrity sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg== - "@esbuild/freebsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== -"@esbuild/freebsd-x64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.4.tgz#873edc0f73e83a82432460ea59bf568c1e90b268" - integrity sha512-3sRbQ6W5kAiVQRBWREGJNd1YE7OgzS0AmOGjDmX/qZZecq8NFlQsQH0IfXjjmD0XtUYqr64e0EKNFjMUlPL3Cw== +"@esbuild/freebsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" + integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== "@esbuild/freebsd-x64@0.20.0": version "0.20.0" @@ -3352,6 +3357,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz#69bd8511fa013b59f0226d1609ac43f7ce489730" integrity sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g== +"@esbuild/freebsd-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz#8c35873fab8c0857a75300a3dcce4324ca0b9844" + integrity sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA== + "@esbuild/freebsd-x64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz#91da08db8bd1bff5f31924c57a81dab26e93a143" @@ -3362,20 +3372,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz#71c77812042a1a8190c3d581e140d15b876b9c6f" integrity sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw== -"@esbuild/freebsd-x64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz#9feb8e826735c568ebfd94859b22a3fbb6a9bdd2" - integrity sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ== - "@esbuild/linux-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== -"@esbuild/linux-arm64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.4.tgz#659f2fa988d448dbf5010b5cc583be757cc1b914" - integrity sha512-ZWmWORaPbsPwmyu7eIEATFlaqm0QGt+joRE9sKcnVUG3oBbr/KYdNE2TnkzdQwX6EDRdg/x8Q4EZQTXoClUqqA== +"@esbuild/linux-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" + integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== "@esbuild/linux-arm64@0.20.0": version "0.20.0" @@ -3397,6 +3402,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz#8050af6d51ddb388c75653ef9871f5ccd8f12383" integrity sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g== +"@esbuild/linux-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz#3edc2f87b889a15b4cedaf65f498c2bed7b16b90" + integrity sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ== + "@esbuild/linux-arm64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz#efc15e45c945a082708f9a9f73bfa8d4db49728a" @@ -3407,20 +3417,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz#f7b7c8f97eff8ffd2e47f6c67eb5c9765f2181b8" integrity sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg== -"@esbuild/linux-arm64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz#c07cbed8e249f4c28e7f32781d36fc4695293d28" - integrity sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ== - "@esbuild/linux-arm@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== -"@esbuild/linux-arm@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.4.tgz#d5b13a7ec1f1c655ce05c8d319b3950797baee55" - integrity sha512-z/4ArqOo9EImzTi4b6Vq+pthLnepFzJ92BnofU1jgNlcVb+UqynVFdoXMCFreTK7FdhqAzH0vmdwW5373Hm9pg== +"@esbuild/linux-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" + integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== "@esbuild/linux-arm@0.20.0": version "0.20.0" @@ -3442,6 +3447,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz#ecaabd1c23b701070484990db9a82f382f99e771" integrity sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ== +"@esbuild/linux-arm@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz#86501cfdfb3d110176d80c41b27ed4611471cde7" + integrity sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg== + "@esbuild/linux-arm@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz#9b93c3e54ac49a2ede6f906e705d5d906f6db9e8" @@ -3452,20 +3462,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz#2a0be71b6cd8201fa559aea45598dffabc05d911" integrity sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw== -"@esbuild/linux-arm@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz#d6e2cd8ef3196468065d41f13fa2a61aaa72644a" - integrity sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw== - "@esbuild/linux-ia32@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== -"@esbuild/linux-ia32@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.4.tgz#878cd8bf24c9847c77acdb5dd1b2ef6e4fa27a82" - integrity sha512-EGc4vYM7i1GRUIMqRZNCTzJh25MHePYsnQfKDexD8uPTCm9mK56NIL04LUfX2aaJ+C9vyEp2fJ7jbqFEYgO9lQ== +"@esbuild/linux-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" + integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== "@esbuild/linux-ia32@0.20.0": version "0.20.0" @@ -3487,6 +3492,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz#3ed2273214178109741c09bd0687098a0243b333" integrity sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ== +"@esbuild/linux-ia32@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz#e6589877876142537c6864680cd5d26a622b9d97" + integrity sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ== + "@esbuild/linux-ia32@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz#be8ef2c3e1d99fca2d25c416b297d00360623596" @@ -3497,11 +3507,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz#763414463cd9ea6fa1f96555d2762f9f84c61783" integrity sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA== -"@esbuild/linux-ia32@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz#3e682bd47c4eddcc4b8f1393dfc8222482f17997" - integrity sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw== - "@esbuild/linux-loong64@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239" @@ -3517,10 +3522,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== -"@esbuild/linux-loong64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.4.tgz#df890499f6e566b7de3aa2361be6df2b8d5fa015" - integrity sha512-WVhIKO26kmm8lPmNrUikxSpXcgd6HDog0cx12BUfA2PkmURHSgx9G6vA19lrlQOMw+UjMZ+l3PpbtzffCxFDRg== +"@esbuild/linux-loong64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" + integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== "@esbuild/linux-loong64@0.20.0": version "0.20.0" @@ -3542,6 +3547,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz#a0fdf440b5485c81b0fbb316b08933d217f5d3ac" integrity sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw== +"@esbuild/linux-loong64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz#11119e18781f136d8083ea10eb6be73db7532de8" + integrity sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg== + "@esbuild/linux-loong64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz#b0840a2707c3fc02eec288d3f9defa3827cd7a87" @@ -3552,20 +3562,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz#428cf2213ff786a502a52c96cf29d1fcf1eb8506" integrity sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg== -"@esbuild/linux-loong64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz#473f5ea2e52399c08ad4cd6b12e6dbcddd630f05" - integrity sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg== - "@esbuild/linux-mips64el@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== -"@esbuild/linux-mips64el@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.4.tgz#76eae4e88d2ce9f4f1b457e93892e802851b6807" - integrity sha512-keYY+Hlj5w86hNp5JJPuZNbvW4jql7c1eXdBUHIJGTeN/+0QFutU3GrS+c27L+NTmzi73yhtojHk+lr2+502Mw== +"@esbuild/linux-mips64el@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" + integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== "@esbuild/linux-mips64el@0.20.0": version "0.20.0" @@ -3587,6 +3592,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz#e11a2806346db8375b18f5e104c5a9d4e81807f6" integrity sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q== +"@esbuild/linux-mips64el@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz#3052f5436b0c0c67a25658d5fc87f045e7def9e6" + integrity sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA== + "@esbuild/linux-mips64el@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz#2a198e5a458c9f0e75881a4e63d26ba0cf9df39f" @@ -3597,20 +3607,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz#5cbcc7fd841b4cd53358afd33527cd394e325d96" integrity sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg== -"@esbuild/linux-mips64el@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz#9960631c9fd61605b0939c19043acf4ef2b51718" - integrity sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw== - "@esbuild/linux-ppc64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== -"@esbuild/linux-ppc64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.4.tgz#c49032f4abbcfa3f747b543a106931fe3dce41ff" - integrity sha512-tQ92n0WMXyEsCH4m32S21fND8VxNiVazUbU4IUGVXQpWiaAxOBvtOtbEt3cXIV3GEBydYsY8pyeRMJx9kn3rvw== +"@esbuild/linux-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" + integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== "@esbuild/linux-ppc64@0.20.0": version "0.20.0" @@ -3632,6 +3637,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz#06a2744c5eaf562b1a90937855b4d6cf7c75ec96" integrity sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw== +"@esbuild/linux-ppc64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz#2f098920ee5be2ce799f35e367b28709925a8744" + integrity sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA== + "@esbuild/linux-ppc64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz#64f4ae0b923d7dd72fb860b9b22edb42007cf8f5" @@ -3642,20 +3652,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz#0d954ab39ce4f5e50f00c4f8c4fd38f976c13ad9" integrity sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ== -"@esbuild/linux-ppc64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz#477cbf8bb04aa034b94f362c32c86b5c31db8d3e" - integrity sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw== - "@esbuild/linux-riscv64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== -"@esbuild/linux-riscv64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.4.tgz#0f815a090772138503ee0465a747e16865bf94b1" - integrity sha512-tRRBey6fG9tqGH6V75xH3lFPpj9E8BH+N+zjSUCnFOX93kEzqS0WdyJHkta/mmJHn7MBaa++9P4ARiU4ykjhig== +"@esbuild/linux-riscv64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" + integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== "@esbuild/linux-riscv64@0.20.0": version "0.20.0" @@ -3677,6 +3682,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz#65b46a2892fc0d1af4ba342af3fe0fa4a8fe08e7" integrity sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA== +"@esbuild/linux-riscv64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz#fa51d7fd0a22a62b51b4b94b405a3198cf7405dd" + integrity sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA== + "@esbuild/linux-riscv64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz#fb2844b11fdddd39e29d291c7cf80f99b0d5158d" @@ -3687,20 +3697,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz#0e7dd30730505abd8088321e8497e94b547bfb1e" integrity sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA== -"@esbuild/linux-riscv64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz#bcdb46c8fb8e93aa779e9a0a62cd4ac00dcac626" - integrity sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w== - "@esbuild/linux-s390x@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== -"@esbuild/linux-s390x@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.4.tgz#8d2cca20cd4e7c311fde8701d9f1042664f8b92b" - integrity sha512-152aLpQqKZYhThiJ+uAM4PcuLCAOxDsCekIbnGzPKVBRUDlgaaAfaUl5NYkB1hgY6WN4sPkejxKlANgVcGl9Qg== +"@esbuild/linux-s390x@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" + integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== "@esbuild/linux-s390x@0.20.0": version "0.20.0" @@ -3722,6 +3727,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz#e71ea18c70c3f604e241d16e4e5ab193a9785d6f" integrity sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw== +"@esbuild/linux-s390x@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz#a27642e36fc282748fdb38954bd3ef4f85791e8a" + integrity sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew== + "@esbuild/linux-s390x@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz#1466876e0aa3560c7673e63fdebc8278707bc750" @@ -3732,20 +3742,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz#5669af81327a398a336d7e40e320b5bbd6e6e72d" integrity sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ== -"@esbuild/linux-s390x@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz#f412cf5fdf0aea849ff51c73fd817c6c0234d46d" - integrity sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw== - "@esbuild/linux-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== -"@esbuild/linux-x64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.4.tgz#f618bec2655de49bff91c588777e37b5e3169d4a" - integrity sha512-Mi4aNA3rz1BNFtB7aGadMD0MavmzuuXNTaYL6/uiYIs08U7YMPETpgNn5oue3ICr+inKwItOwSsJDYkrE9ekVg== +"@esbuild/linux-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" + integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== "@esbuild/linux-x64@0.20.0": version "0.20.0" @@ -3767,6 +3772,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz#d47f97391e80690d4dfe811a2e7d6927ad9eed24" integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ== +"@esbuild/linux-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz#9d9b09c0033d17529570ced6d813f98315dfe4e9" + integrity sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA== + "@esbuild/linux-x64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz#c10fde899455db7cba5f11b3bccfa0e41bf4d0cd" @@ -3777,10 +3787,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz#b2357dd153aa49038967ddc1ffd90c68a9d2a0d4" integrity sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw== -"@esbuild/linux-x64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz#d8233c09b5ebc0c855712dc5eeb835a3a3341108" - integrity sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig== +"@esbuild/netbsd-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz#25c09a659c97e8af19e3f2afd1c9190435802151" + integrity sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A== "@esbuild/netbsd-arm64@0.25.4": version "0.25.4" @@ -3792,20 +3802,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz#53b4dfb8fe1cee93777c9e366893bd3daa6ba63d" integrity sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw== -"@esbuild/netbsd-arm64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz#f51ae8dd1474172e73cf9cbaf8a38d1c72dd8f1a" - integrity sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q== - "@esbuild/netbsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== -"@esbuild/netbsd-x64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.4.tgz#7889744ca4d60f1538d62382b95e90a49687cef2" - integrity sha512-9+Wxx1i5N/CYo505CTT7T+ix4lVzEdz0uCoYGxM5JDVlP2YdDC1Bdz+Khv6IbqmisT0Si928eAxbmGkcbiuM/A== +"@esbuild/netbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" + integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== "@esbuild/netbsd-x64@0.20.0": version "0.20.0" @@ -3827,6 +3832,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz#44e743c9778d57a8ace4b72f3c6b839a3b74a653" integrity sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA== +"@esbuild/netbsd-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz#7fa5f6ffc19be3a0f6f5fd32c90df3dc2506937a" + integrity sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig== + "@esbuild/netbsd-x64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz#ec401fb0b1ed0ac01d978564c5fc8634ed1dc2ed" @@ -3837,16 +3847,16 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz#a0206f6314ce7dc8713b7732703d0f58de1d1e79" integrity sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ== -"@esbuild/netbsd-x64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz#a267538602c0e50a858cf41dcfe5d8036f8da8e7" - integrity sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g== - "@esbuild/openbsd-arm64@0.23.1": version "0.23.1" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz#05c5a1faf67b9881834758c69f3e51b7dee015d7" integrity sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q== +"@esbuild/openbsd-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz#8faa6aa1afca0c6d024398321d6cb1c18e72a1c3" + integrity sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw== + "@esbuild/openbsd-arm64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz#f272c2f41cfea1d91b93d487a51b5c5ca7a8c8c4" @@ -3857,20 +3867,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz#2a796c87c44e8de78001d808c77d948a21ec22fd" integrity sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw== -"@esbuild/openbsd-arm64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz#a51be60c425b85c216479b8c344ad0511635f2d2" - integrity sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg== - "@esbuild/openbsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== -"@esbuild/openbsd-x64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.4.tgz#c3e436eb9271a423d2e8436fcb120e3fd90e2b01" - integrity sha512-MFsHleM5/rWRW9EivFssop+OulYVUoVcqkyOkjiynKBCGBj9Lihl7kh9IzrreDyXa4sNkquei5/DTP4uCk25xw== +"@esbuild/openbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" + integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== "@esbuild/openbsd-x64@0.20.0": version "0.20.0" @@ -3892,6 +3897,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz#2e58ae511bacf67d19f9f2dcd9e8c5a93f00c273" integrity sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA== +"@esbuild/openbsd-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz#a42979b016f29559a8453d32440d3c8cd420af5e" + integrity sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw== + "@esbuild/openbsd-x64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz#2e25950bc10fa9db1e5c868e3d50c44f7c150fd7" @@ -3902,25 +3912,20 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz#28d0cd8909b7fa3953af998f2b2ed34f576728f0" integrity sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg== -"@esbuild/openbsd-x64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz#7e4a743c73f75562e29223ba69d0be6c9c9008da" - integrity sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw== - -"@esbuild/openharmony-arm64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz#2087a5028f387879154ebf44bdedfafa17682e5b" - integrity sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA== +"@esbuild/openharmony-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz#fd87bfeadd7eeb3aa384bbba907459ffa3197cb1" + integrity sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag== "@esbuild/sunos-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== -"@esbuild/sunos-x64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.4.tgz#f63f5841ba8c8c1a1c840d073afc99b53e8ce740" - integrity sha512-6Xq8SpK46yLvrGxjp6HftkDwPP49puU4OF0hEL4dTxqCbfx09LyrbUj/D7tmIRMj5D5FCUPksBbxyQhp8tmHzw== +"@esbuild/sunos-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" + integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== "@esbuild/sunos-x64@0.20.0": version "0.20.0" @@ -3942,6 +3947,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz#adb022b959d18d3389ac70769cef5a03d3abd403" integrity sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA== +"@esbuild/sunos-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz#3a18f590e36cb78ae7397976b760b2b8c74407f4" + integrity sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ== + "@esbuild/sunos-x64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz#cd596fa65a67b3b7adc5ecd52d9f5733832e1abd" @@ -3952,20 +3962,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz#a28164f5b997e8247d407e36c90d3fd5ddbe0dc5" integrity sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA== -"@esbuild/sunos-x64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz#56531f861723ea0dc6283a2bb8837304223cb736" - integrity sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA== - "@esbuild/win32-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== -"@esbuild/win32-arm64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.4.tgz#80be69cec92da4da7781cf7a8351b95cc5a236b0" - integrity sha512-PkIl7Jq4mP6ke7QKwyg4fD4Xvn8PXisagV/+HntWoDEdmerB2LTukRZg728Yd1Fj+LuEX75t/hKXE2Ppk8Hh1w== +"@esbuild/win32-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" + integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== "@esbuild/win32-arm64@0.20.0": version "0.20.0" @@ -3987,6 +3992,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz#84906f50c212b72ec360f48461d43202f4c8b9a2" integrity sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A== +"@esbuild/win32-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz#e71741a251e3fd971408827a529d2325551f530c" + integrity sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw== + "@esbuild/win32-arm64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz#b4dbcb57b21eeaf8331e424c3999b89d8951dc88" @@ -3997,20 +4007,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz#6eadbead38e8bd12f633a5190e45eff80e24007e" integrity sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw== -"@esbuild/win32-arm64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz#f4989f033deac6fae323acff58764fa8bc01436e" - integrity sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q== - "@esbuild/win32-ia32@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== -"@esbuild/win32-ia32@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.4.tgz#15dc0ed83d2794872b05d8edc4a358fecf97eb54" - integrity sha512-ga676Hnvw7/ycdKB53qPusvsKdwrWzEyJ+AtItHGoARszIqvjffTwaaW3b2L6l90i7MO9i+dlAW415INuRhSGg== +"@esbuild/win32-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" + integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== "@esbuild/win32-ia32@0.20.0": version "0.20.0" @@ -4032,6 +4037,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz#5e3eacc515820ff729e90d0cb463183128e82fac" integrity sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ== +"@esbuild/win32-ia32@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz#c6f010b5d3b943d8901a0c87ea55f93b8b54bf94" + integrity sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw== + "@esbuild/win32-ia32@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz#410842e5d66d4ece1757634e297a87635eb82f7a" @@ -4042,20 +4052,15 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz#bab6288005482f9ed2adb9ded7e88eba9a62cc0d" integrity sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ== -"@esbuild/win32-ia32@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz#b260e9df71e3939eb33925076d39f63cec7d1525" - integrity sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ== - "@esbuild/win32-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== -"@esbuild/win32-x64@0.19.4": - version "0.19.4" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.4.tgz#d46a6e220a717f31f39ae80f49477cc3220be0f0" - integrity sha512-HP0GDNla1T3ZL8Ko/SHAS2GgtjOg+VmWnnYLhuTksr++EnduYB0f3Y2LzHsUwb2iQ13JGoY6G3R8h6Du/WG6uA== +"@esbuild/win32-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" + integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== "@esbuild/win32-x64@0.20.0": version "0.20.0" @@ -4077,6 +4082,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz#81fd50d11e2c32b2d6241470e3185b70c7b30699" integrity sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg== +"@esbuild/win32-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz#e4b3e255a1b4aea84f6e1d2ae0b73f826c3785bd" + integrity sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw== + "@esbuild/win32-x64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz#0b17ec8a70b2385827d52314c1253160a0b9bacc" @@ -4087,11 +4097,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1" integrity sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g== -"@esbuild/win32-x64@0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz#4276edd5c105bc28b11c6a1f76fb9d29d1bd25c1" - integrity sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA== - "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.9.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3" @@ -8863,14 +8868,6 @@ resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.11.tgz#70b7131f65038ae63cfe841354c8aba363632344" integrity sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg== -"@types/node-fetch@^2.6.11": - version "2.6.11" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" - integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== - dependencies: - "@types/node" "*" - form-data "^4.0.0" - "@types/node-forge@^1.3.0": version "1.3.11" resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" @@ -10507,12 +10504,12 @@ ansi-html@^0.0.7: ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== ansi-regex@^4.1.0: version "4.1.1" @@ -10525,9 +10522,9 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1: integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== ansi-styles@^2.2.1: version "2.2.1" @@ -11204,7 +11201,7 @@ autoprefixer@^10.4.13, autoprefixer@^10.4.19, autoprefixer@^10.4.20, autoprefixe picocolors "^1.0.1" postcss-value-parser "^4.2.0" -available-typed-arrays@^1.0.2, available-typed-arrays@^1.0.7: +available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== @@ -12534,7 +12531,7 @@ calculate-cache-key-for-tree@^2.0.0: dependencies: json-stable-stringify "^1.0.1" -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== @@ -12542,18 +12539,17 @@ call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: es-errors "^1.3.0" function-bind "^1.1.2" -call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== dependencies: + call-bind-apply-helpers "^1.0.0" es-define-property "^1.0.0" - es-errors "^1.3.0" - function-bind "^1.1.2" get-intrinsic "^1.2.4" - set-function-length "^1.2.1" + set-function-length "^1.2.2" -call-bound@^1.0.2: +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== @@ -13698,9 +13694,9 @@ cross-argv@^2.0.0: integrity sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg== cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + version "6.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57" + integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw== dependencies: nice-try "^1.0.4" path-key "^2.0.1" @@ -14436,14 +14432,14 @@ deterministic-object-hash@^1.3.1: integrity sha512-kQDIieBUreEgY+akq0N7o4FzZCr27dPG1xr3wq267vPwDlSXQ3UMcBXHqTGUBaM/5WDS1jwTYjxRhUzHeuiAvw== devalue@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/devalue/-/devalue-4.3.2.tgz#cc44e4cf3872ac5a78229fbce3b77e57032727b5" - integrity sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg== + version "4.3.3" + resolved "https://registry.yarnpkg.com/devalue/-/devalue-4.3.3.tgz#e35df3bdc49136837e77986f629b9fa6fef50726" + integrity sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg== devalue@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.0.0.tgz#1ca0099a7d715b4d6cac3924e770ccbbc584ad98" - integrity sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA== + version "5.3.2" + resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.3.2.tgz#1d9a00f0d126a2f768589f236da8b67d6988d285" + integrity sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw== devlop@^1.0.0: version "1.1.0" @@ -15959,32 +15955,33 @@ esbuild@^0.18.10: "@esbuild/win32-x64" "0.18.20" esbuild@^0.19.2: - version "0.19.4" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.4.tgz#cdf5c4c684956d550bc3c6d0c01dac7fef6c75b1" - integrity sha512-x7jL0tbRRpv4QUyuDMjONtWFciygUxWaUM1kMX2zWxI0X2YWOt7MSA0g4UdeSiHM8fcYVzpQhKYOycZwxTdZkA== + version "0.19.12" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" + integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg== optionalDependencies: - "@esbuild/android-arm" "0.19.4" - "@esbuild/android-arm64" "0.19.4" - "@esbuild/android-x64" "0.19.4" - "@esbuild/darwin-arm64" "0.19.4" - "@esbuild/darwin-x64" "0.19.4" - "@esbuild/freebsd-arm64" "0.19.4" - "@esbuild/freebsd-x64" "0.19.4" - "@esbuild/linux-arm" "0.19.4" - "@esbuild/linux-arm64" "0.19.4" - "@esbuild/linux-ia32" "0.19.4" - "@esbuild/linux-loong64" "0.19.4" - "@esbuild/linux-mips64el" "0.19.4" - "@esbuild/linux-ppc64" "0.19.4" - "@esbuild/linux-riscv64" "0.19.4" - "@esbuild/linux-s390x" "0.19.4" - "@esbuild/linux-x64" "0.19.4" - "@esbuild/netbsd-x64" "0.19.4" - "@esbuild/openbsd-x64" "0.19.4" - "@esbuild/sunos-x64" "0.19.4" - "@esbuild/win32-arm64" "0.19.4" - "@esbuild/win32-ia32" "0.19.4" - "@esbuild/win32-x64" "0.19.4" + "@esbuild/aix-ppc64" "0.19.12" + "@esbuild/android-arm" "0.19.12" + "@esbuild/android-arm64" "0.19.12" + "@esbuild/android-x64" "0.19.12" + "@esbuild/darwin-arm64" "0.19.12" + "@esbuild/darwin-x64" "0.19.12" + "@esbuild/freebsd-arm64" "0.19.12" + "@esbuild/freebsd-x64" "0.19.12" + "@esbuild/linux-arm" "0.19.12" + "@esbuild/linux-arm64" "0.19.12" + "@esbuild/linux-ia32" "0.19.12" + "@esbuild/linux-loong64" "0.19.12" + "@esbuild/linux-mips64el" "0.19.12" + "@esbuild/linux-ppc64" "0.19.12" + "@esbuild/linux-riscv64" "0.19.12" + "@esbuild/linux-s390x" "0.19.12" + "@esbuild/linux-x64" "0.19.12" + "@esbuild/netbsd-x64" "0.19.12" + "@esbuild/openbsd-x64" "0.19.12" + "@esbuild/sunos-x64" "0.19.12" + "@esbuild/win32-arm64" "0.19.12" + "@esbuild/win32-ia32" "0.19.12" + "@esbuild/win32-x64" "0.19.12" esbuild@^0.20.2: version "0.20.2" @@ -16075,36 +16072,36 @@ esbuild@^0.23.0, esbuild@^0.23.1: "@esbuild/win32-x64" "0.23.1" esbuild@^0.25.0, esbuild@^0.25.5: - version "0.25.6" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.6.tgz#9b82a3db2fa131aec069ab040fd57ed0a880cdcd" - integrity sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg== + version "0.25.10" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.10.tgz#37f5aa5cd14500f141be121c01b096ca83ac34a9" + integrity sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ== optionalDependencies: - "@esbuild/aix-ppc64" "0.25.6" - "@esbuild/android-arm" "0.25.6" - "@esbuild/android-arm64" "0.25.6" - "@esbuild/android-x64" "0.25.6" - "@esbuild/darwin-arm64" "0.25.6" - "@esbuild/darwin-x64" "0.25.6" - "@esbuild/freebsd-arm64" "0.25.6" - "@esbuild/freebsd-x64" "0.25.6" - "@esbuild/linux-arm" "0.25.6" - "@esbuild/linux-arm64" "0.25.6" - "@esbuild/linux-ia32" "0.25.6" - "@esbuild/linux-loong64" "0.25.6" - "@esbuild/linux-mips64el" "0.25.6" - "@esbuild/linux-ppc64" "0.25.6" - "@esbuild/linux-riscv64" "0.25.6" - "@esbuild/linux-s390x" "0.25.6" - "@esbuild/linux-x64" "0.25.6" - "@esbuild/netbsd-arm64" "0.25.6" - "@esbuild/netbsd-x64" "0.25.6" - "@esbuild/openbsd-arm64" "0.25.6" - "@esbuild/openbsd-x64" "0.25.6" - "@esbuild/openharmony-arm64" "0.25.6" - "@esbuild/sunos-x64" "0.25.6" - "@esbuild/win32-arm64" "0.25.6" - "@esbuild/win32-ia32" "0.25.6" - "@esbuild/win32-x64" "0.25.6" + "@esbuild/aix-ppc64" "0.25.10" + "@esbuild/android-arm" "0.25.10" + "@esbuild/android-arm64" "0.25.10" + "@esbuild/android-x64" "0.25.10" + "@esbuild/darwin-arm64" "0.25.10" + "@esbuild/darwin-x64" "0.25.10" + "@esbuild/freebsd-arm64" "0.25.10" + "@esbuild/freebsd-x64" "0.25.10" + "@esbuild/linux-arm" "0.25.10" + "@esbuild/linux-arm64" "0.25.10" + "@esbuild/linux-ia32" "0.25.10" + "@esbuild/linux-loong64" "0.25.10" + "@esbuild/linux-mips64el" "0.25.10" + "@esbuild/linux-ppc64" "0.25.10" + "@esbuild/linux-riscv64" "0.25.10" + "@esbuild/linux-s390x" "0.25.10" + "@esbuild/linux-x64" "0.25.10" + "@esbuild/netbsd-arm64" "0.25.10" + "@esbuild/netbsd-x64" "0.25.10" + "@esbuild/openbsd-arm64" "0.25.10" + "@esbuild/openbsd-x64" "0.25.10" + "@esbuild/openharmony-arm64" "0.25.10" + "@esbuild/sunos-x64" "0.25.10" + "@esbuild/win32-arm64" "0.25.10" + "@esbuild/win32-ia32" "0.25.10" + "@esbuild/win32-x64" "0.25.10" escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" @@ -17323,23 +17320,18 @@ follow-redirects@^1.0.0, follow-redirects@^1.15.6, follow-redirects@^1.15.9: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== +for-each@^0.3.3, for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== dependencies: - is-callable "^1.1.3" + is-callable "^1.2.7" for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= - foreground-child@^3.1.0: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" @@ -18911,9 +18903,9 @@ http-proxy-agent@^7.0.0: debug "^4.3.4" http-proxy-middleware@^2.0.3: - version "2.0.7" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" - integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== + version "2.0.9" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" + integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" @@ -19460,7 +19452,7 @@ is-builtin-module@^3.1.0, is-builtin-module@^3.2.1: dependencies: builtin-modules "^3.3.0" -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: +is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -19809,16 +19801,12 @@ is-type@0.0.1: dependencies: core-util-is "~1.0.0" -is-typed-array@^1.1.3: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.5.tgz#f32e6e096455e329eb7b423862456aa213f0eb4e" - integrity sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug== +is-typed-array@^1.1.14, is-typed-array@^1.1.3: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== dependencies: - available-typed-arrays "^1.0.2" - call-bind "^1.0.2" - es-abstract "^1.18.0-next.2" - foreach "^2.0.5" - has-symbols "^1.0.1" + which-typed-array "^1.1.16" is-typedarray@^1.0.0: version "1.0.0" @@ -20282,18 +20270,18 @@ json-stringify-safe@^5.0.1: json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= + integrity sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw== json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" json5@^2.1.2, json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: version "2.2.3" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonc-parser@3.1.0: @@ -24606,7 +24594,7 @@ path-to-regexp@6.3.0, path-to-regexp@^6.2.0, path-to-regexp@^6.2.1: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== -path-to-regexp@8.2.0, path-to-regexp@^8.0.0, path-to-regexp@^8.1.0: +path-to-regexp@8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== @@ -24618,6 +24606,11 @@ path-to-regexp@^1.5.3, path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^8.0.0, path-to-regexp@^8.1.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f" + integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== + path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -27252,7 +27245,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -27593,7 +27586,7 @@ set-cookie-parser@^2.4.8, set-cookie-parser@^2.6.0: resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== -set-function-length@^1.2.1: +set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -27641,12 +27634,13 @@ setprototypeof@1.2.0: integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== sha.js@^2.4.11: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + version "2.4.12" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f" + integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w== dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" + inherits "^2.0.4" + safe-buffer "^5.2.1" + to-buffer "^1.2.0" shallow-clone@^3.0.0: version "3.0.1" @@ -29043,9 +29037,9 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== tar-fs@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92" - integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg== + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930" + integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ== dependencies: chownr "^1.1.1" mkdirp-classic "^0.5.2" @@ -29053,9 +29047,9 @@ tar-fs@^2.0.0: tar-stream "^2.1.4" tar-fs@^3.0.4: - version "3.0.10" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.10.tgz#60f8ccd60fe30164bdd3d6606619650236ed38f7" - integrity sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA== + version "3.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.1.1.tgz#4f164e59fb60f103d472360731e8c6bb4a7fe9ef" + integrity sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg== dependencies: pump "^3.0.0" tar-stream "^3.1.5" @@ -29426,7 +29420,7 @@ tmp-promise@^3.0.2: tmp@0.0.28: version "0.0.28" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" - integrity sha1-Fyc1t/YU6nrzlmT6hM8N5OUV0SA= + integrity sha512-c2mmfiBmND6SOVxzogm1oda0OJ1HZVIk/5n26N59dDTh80MUeavpiCls4PGAdkX1PFkKokLpcf7prSjCeXLsJg== dependencies: os-tmpdir "~1.0.1" @@ -29445,15 +29439,24 @@ tmp@^0.1.0: rimraf "^2.6.3" tmp@^0.2.0, tmp@^0.2.1, tmp@~0.2.1: - version "0.2.3" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" - integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + version "0.2.5" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== tmpl@1.0.x: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== +to-buffer@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.2.tgz#ffe59ef7522ada0a2d1cb5dfe03bb8abc3cdc133" + integrity sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw== + dependencies: + isarray "^2.0.5" + safe-buffer "^5.2.1" + typed-array-buffer "^1.0.3" + to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -29827,6 +29830,15 @@ type-level-regexp@~0.1.17: resolved "https://registry.yarnpkg.com/type-level-regexp/-/type-level-regexp-0.1.17.tgz#ec1bf7dd65b85201f9863031d6f023bdefc2410f" integrity sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg== +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + typed-assert@^1.0.8: version "1.0.9" resolved "https://registry.yarnpkg.com/typed-assert/-/typed-assert-1.0.9.tgz#8af9d4f93432c4970ec717e3006f33f135b06213" @@ -30007,9 +30019,9 @@ undici@^5.25.4, undici@^5.28.5: "@fastify/busboy" "^2.0.0" undici@^6.11.1, undici@^6.19.2: - version "6.21.1" - resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.1.tgz#336025a14162e6837e44ad7b819b35b6c6af0e05" - integrity sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ== + version "6.21.3" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.3.tgz#185752ad92c3d0efe7a7d1f6854a50f83b552d7a" + integrity sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw== unenv@2.0.0-rc.17: version "2.0.0-rc.17" @@ -31484,15 +31496,17 @@ which-pm@^2.1.1: load-yaml-file "^0.2.0" path-exists "^4.0.0" -which-typed-array@^1.1.13, which-typed-array@^1.1.2: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== +which-typed-array@^1.1.13, which-typed-array@^1.1.16, which-typed-array@^1.1.2: + version "1.1.19" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== dependencies: available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" has-tostringtag "^1.0.2" which@^1.2.14, which@^1.2.9: From 49badb04a655513fdb0bd87ae949ce13d8c60f75 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 19 Nov 2025 16:03:13 +0100 Subject: [PATCH 08/32] feat(core): Add `gibibyte` and `pebibyte` to `InformationUnit` type (#18241) Both of these units are supported by Relay, see https://getsentry.github.io/relay/relay_metrics/enum.InformationUnit.html. --- packages/core/src/types-hoist/measurement.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/types-hoist/measurement.ts b/packages/core/src/types-hoist/measurement.ts index 8aea675d5254..40649ddab439 100644 --- a/packages/core/src/types-hoist/measurement.ts +++ b/packages/core/src/types-hoist/measurement.ts @@ -17,9 +17,11 @@ export type InformationUnit = | 'megabyte' | 'mebibyte' | 'gigabyte' + | 'gibibyte' | 'terabyte' | 'tebibyte' | 'petabyte' + | 'pebibyte' | 'exabyte' | 'exbibyte'; From 93faf616244c7ca7480f2a738543923f54b01b78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:42:40 +0000 Subject: [PATCH 09/32] chore(deps): bump glob from 11.0.1 to 11.1.0 in /packages/react-router (#18243) --- packages/react-router/package.json | 2 +- yarn.lock | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 3d57ad54469f..d988e14aea8a 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -55,7 +55,7 @@ "@sentry/node": "10.26.0", "@sentry/react": "10.26.0", "@sentry/vite-plugin": "^4.1.0", - "glob": "11.0.1" + "glob": "11.1.0" }, "devDependencies": { "@react-router/dev": "^7.5.2", diff --git a/yarn.lock b/yarn.lock index c93b8a45e748..93c288553b5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17332,7 +17332,7 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= -foreground-child@^3.1.0: +foreground-child@^3.1.0, foreground-child@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== @@ -17907,14 +17907,14 @@ glob-to-regexp@0.4.1, glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@11.0.1: - version "11.0.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.1.tgz#1c3aef9a59d680e611b53dcd24bb8639cef064d9" - integrity sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw== +glob@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.1.0.tgz#4f826576e4eb99c7dad383793d2f9f08f67e50a6" + integrity sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw== dependencies: - foreground-child "^3.1.0" - jackspeak "^4.0.1" - minimatch "^10.0.0" + foreground-child "^3.3.1" + jackspeak "^4.1.1" + minimatch "^10.1.1" minipass "^7.1.2" package-json-from-dist "^1.0.0" path-scurry "^2.0.0" @@ -20023,7 +20023,7 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -jackspeak@^4.0.1: +jackspeak@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae" integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ== @@ -22289,10 +22289,10 @@ minimatch@5.1.0, minimatch@^5.0.1, minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" -minimatch@^10.0.0: - version "10.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" - integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== +minimatch@^10.1.1: + version "10.1.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" + integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== dependencies: "@isaacs/brace-expansion" "^5.0.0" From 6d75c8f2894c79596a8489b87cdd0315bf4e7dd4 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 20 Nov 2025 10:24:44 +0100 Subject: [PATCH 10/32] fix(metrics): Update return type of `beforeSendMetric` (#18261) --- packages/core/src/types-hoist/options.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index ccdc3b180e15..59c4609f01c4 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -420,9 +420,9 @@ export interface ClientOptions Metric; + beforeSendMetric?: (metric: Metric) => Metric | null; /** * Function to compute tracing sample rate dynamically and filter unwanted traces. From 5503d96ed85d6a32383f702456526a4f83e04753 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:17:17 +0100 Subject: [PATCH 11/32] chore(deps): bump astro from 4.16.18 to 5.15.9 in /dev-packages/e2e-tests/test-applications/cloudflare-astro (#18259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) from 4.16.18 to 5.15.9.
Release notes

Sourced from astro's releases.

astro@5.15.9

Patch Changes

  • #14786 758a891 Thanks @​mef! - Add handling of invalid encrypted props and slots in server islands.

  • #14783 504958f Thanks @​florian-lefebvre! - Improves the experimental Fonts API build log to show the number of downloaded files. This can help spotting excessive downloading because of misconfiguration

  • #14791 9e9c528 Thanks @​Princesseuh! - Changes the remote protocol checks for images to require explicit authorization in order to use data URIs.

    In order to allow data URIs for remote images, you will need to update your astro.config.mjs file to include the following configuration:

    // astro.config.mjs
    import { defineConfig } from 'astro/config';
    

    export default defineConfig({ images: { remotePatterns: [ { protocol: 'data', }, ], }, });

  • #14787 0f75f6b Thanks @​matthewp! - Fixes wildcard hostname pattern matching to correctly reject hostnames without dots

    Previously, hostnames like localhost or other single-part names would incorrectly match patterns like *.example.com. The wildcard matching logic has been corrected to ensure that only valid subdomains matching the pattern are accepted.

  • #14776 3537876 Thanks @​ktym4a! - Fixes the behavior of passthroughImageService so it does not generate webp.

  • Updated dependencies [9e9c528, 0f75f6b]:

    • @​astrojs/internal-helpers@​0.7.5
    • @​astrojs/markdown-remark@​6.3.9

astro@5.15.8

Patch Changes

  • #14772 00c579a Thanks @​matthewp! - Improves the security of Server Islands slots by encrypting them before transmission to the browser, matching the security model used for props. This improves the integrity of slot content and prevents injection attacks, even when component templates don't explicitly support slots.

    Slots continue to work as expected for normal usage—this change has no breaking changes for legitimate requests.

  • #14771 6f80081 Thanks @​matthewp! - Fix middleware pathname matching by normalizing URL-encoded paths

    Middleware now receives normalized pathname values, ensuring that encoded paths like /%61dmin are properly decoded to /admin before middleware checks. This prevents potential security issues where middleware checks might be bypassed through URL encoding.

astro@5.15.7

Patch Changes

... (truncated)

Changelog

Sourced from astro's changelog.

5.15.9

Patch Changes

  • #14786 758a891 Thanks @​mef! - Add handling of invalid encrypted props and slots in server islands.

  • #14783 504958f Thanks @​florian-lefebvre! - Improves the experimental Fonts API build log to show the number of downloaded files. This can help spotting excessive downloading because of misconfiguration

  • #14791 9e9c528 Thanks @​Princesseuh! - Changes the remote protocol checks for images to require explicit authorization in order to use data URIs.

    In order to allow data URIs for remote images, you will need to update your astro.config.mjs file to include the following configuration:

    // astro.config.mjs
    import { defineConfig } from 'astro/config';
    

    export default defineConfig({ images: { remotePatterns: [ { protocol: 'data', }, ], }, });

  • #14787 0f75f6b Thanks @​matthewp! - Fixes wildcard hostname pattern matching to correctly reject hostnames without dots

    Previously, hostnames like localhost or other single-part names would incorrectly match patterns like *.example.com. The wildcard matching logic has been corrected to ensure that only valid subdomains matching the pattern are accepted.

  • #14776 3537876 Thanks @​ktym4a! - Fixes the behavior of passthroughImageService so it does not generate webp.

  • Updated dependencies [9e9c528, 0f75f6b]:

    • @​astrojs/internal-helpers@​0.7.5
    • @​astrojs/markdown-remark@​6.3.9

5.15.8

Patch Changes

  • #14772 00c579a Thanks @​matthewp! - Improves the security of Server Islands slots by encrypting them before transmission to the browser, matching the security model used for props. This improves the integrity of slot content and prevents injection attacks, even when component templates don't explicitly support slots.

    Slots continue to work as expected for normal usage—this change has no breaking changes for legitimate requests.

  • #14771 6f80081 Thanks @​matthewp! - Fix middleware pathname matching by normalizing URL-encoded paths

    Middleware now receives normalized pathname values, ensuring that encoded paths like /%61dmin are properly decoded to /admin before middleware checks. This prevents potential security issues where middleware checks might be bypassed through URL encoding.

5.15.7

... (truncated)

Commits
Maintainer changes

This version was pushed to npm by [GitHub Actions](https://www.npmjs.com/~GitHub Actions), a new releaser for astro since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=astro&package-manager=npm_and_yarn&previous-version=4.16.18&new-version=5.15.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../e2e-tests/test-applications/cloudflare-astro/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json index 5adbcd6ad75f..4db15edabbd7 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json @@ -19,7 +19,7 @@ "dependencies": { "@astrojs/cloudflare": "8.1.0", "@sentry/astro": "latest || *", - "astro": "4.16.18" + "astro": "5.15.9" }, "devDependencies": { "@astrojs/internal-helpers": "0.4.1" From f0d5d76046b476f5ab9beaa75803d2987ba2f435 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:17:34 +0100 Subject: [PATCH 12/32] build(deps): bump hono from 4.9.7 to 4.10.3 in /dev-packages/e2e-tests/test-applications/cloudflare-hono (#18038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [hono](https://github.com/honojs/hono) from 4.9.7 to 4.10.3.
Release notes

Sourced from hono's releases.

v4.10.3

Securiy Fix

A security issue in the CORS middleware has been fixed. In some cases, a request header could affect the Vary response header. Please update to the latest version if you are using the CORS middleware.

What's Changed

New Contributors

Full Changelog: https://github.com/honojs/hono/compare/v4.10.2...v4.10.3

v4.10.2

Security hardening improvement

If you are using JWT middleware, please read the following and consider applying the configuration.

Improper Authorization in Hono (JWT Audience Validation)

Hono’s JWT authentication middleware did not validate the aud (Audience) claim by default. As a result, applications using the middleware without an explicit audience check could accept tokens intended for other audiences, leading to potential cross-service access (token mix-up).

The issue is addressed by adding a new verification.aud configuration option to allow RFC 7519–compliant audience validation. This change is classified as a security hardening improvement, but the lack of validation can still be considered a vulnerability in deployments that rely on default JWT verification.

Recommended secure configuration

You can enable RFC 7519–compliant audience validation using the new verification.aud option:

import { Hono } from 'hono'
import { jwt } from 'hono/jwt'

const app = new Hono()

app.use(
'/api/*',
jwt({
secret: 'my-secret',
verification: {
// Require this API to only accept tokens with aud = 'service-a'
aud: 'service-a',
},
})
)

What's Changed

... (truncated)

Commits
  • fcefd50 4.10.3
  • 95ae4d3 refactor(jwt): reduce the size of the code generated by minification (#4480)
  • d9b8b4b Merge commit from fork
  • 5216117 fix(request-id): validation accepts = (#4478)
  • 253ec28 fix(aws-lambda): serve microsoft office files as binary in lambda handler (#4...
  • 0c6455d 4.10.2
  • 45ba3bf Merge commit from fork
  • 4cbad8b tests: Fix test case of handlers without a path (#4472)
  • db764c2 4.10.1
  • 8774bf9 fix(types): cannot .use non-return mw from createMiddleware (#4465)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hono&package-manager=npm_and_yarn&previous-version=4.9.7&new-version=4.10.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jan Peer Stöcklmair --- .../e2e-tests/test-applications/cloudflare-hono/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json index de22031fdda9..5a582b0aa127 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@sentry/cloudflare": "latest || *", - "hono": "4.9.7" + "hono": "4.10.3" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.8.31", From 28e9cc659914c0239e0a488b56e75560b6885826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Thu, 20 Nov 2025 16:35:45 +0000 Subject: [PATCH 13/32] chore: Do not update opentelemetry (#18254) This disables the creation of dependabot updates for opentelemetry. Based on the image below there was not a real benefit of having this, except noise. Screenshot 2025-11-19 at 11 21 02 --- .github/dependabot.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1df50881932d..66d551fabef8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,9 +14,15 @@ updates: interval: 'weekly' allow: - dependency-name: '@sentry/*' - - dependency-name: '@opentelemetry/*' - - dependency-name: '@prisma/instrumentation' - dependency-name: '@playwright/test' + - dependency-name: '@opentelemetry/*' + ignore: + - dependency-name: '@opentelemetry/instrumentation' + - dependency-name: '@opentelemetry/instrumentation-*' + groups: + opentelemetry: + patterns: + - '@opentelemetry/*' versioning-strategy: increase commit-message: prefix: feat From c7e88d4416f2527b279e6fefb9bc8e36589f11b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Thu, 20 Nov 2025 16:43:26 +0000 Subject: [PATCH 14/32] fix(core): Add a PromiseBuffer for incoming events on the client (#18120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Previously, the client would process all incoming events without any limit, which could lead to unbounded growth of pending events/promises in memory. This could cause performance issues and memory pressure in high-throughput scenarios. This occurs when two conditions are met: - when an integration with an async `processEvent` are added (e.g. `ContextLines`, which is a defaultIntegration) - events, e.g. `Sentry.captureException`, are called synchronously ```js Sentry.init({ ... }); // ... for (let i = 0; i < 5000; i++) { Sentry.captureException(new Error()); } ``` ## Solution This PR adds a `PromiseBuffer` to the `Client` class to limit the number of concurrent event processing operations. - Introduced a `_promiseBuffer` in the `Client` class that limits concurrent event processing - The buffer size defaults to `DEFAULT_TRANSPORT_BUFFER_SIZE` (64) but can be configured via `transportOptions.bufferSize` - When the buffer is full, events are rejected and properly tracked as dropped events with the `queue_overflow` reason - Please tak - Modified the `_process()` method to: - Accept a task producer function instead of a promise directly (lazy evaluation) - Use the promise buffer to manage concurrent operations - Track the data category for proper dropped event categorization ## Special 👀 on - About reusing `transportOptions.bufferSize`: Not sure if this is the best technique, but IMO both should have the same size - because if it wouldn't it would be capped at a later stage (asking myself if the transport still needs the promise buffer - as we have it now way earlier in place) - The `_process` takes now a `DataCategory`. At the time of the process the event type is almost unknown. Not sure if I assumed the categories correctly there, or if there is another technique of getting the type (**edit:** a [comment by Cursor](https://github.com/getsentry/sentry-javascript/pull/18120/files/2ee14b484d00432145d4f9a6773fbd31f92921d7#r2504259236) helped a little and I added [a helper function](https://github.com/getsentry/sentry-javascript/pull/18120/commits/7381a49ac34964d637f56625b2bf48617820b29d)) - `recordDroppedEvent` is now printing it one after each other - theoretically we can count all occurences and print the count on it. I decided against this one, since it would delay the user feedback - this can be challenged though --- .size-limit.js | 10 ++-- packages/core/src/client.ts | 44 ++++++++++++----- packages/core/test/lib/client.test.ts | 65 ++++++++++++++++++++++++- packages/core/test/mocks/integration.ts | 10 ++++ 4 files changed, 112 insertions(+), 17 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 184aad0698f4..100444907e06 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -38,7 +38,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '41.38 KB', + limit: '41.5 KB', }, { name: '@sentry/browser (incl. Tracing, Profiling)', @@ -127,7 +127,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '43.33 KB', + limit: '43.5 KB', }, // Vue SDK (ESM) { @@ -142,7 +142,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '43.2 KB', + limit: '43.3 KB', }, // Svelte SDK (ESM) { @@ -163,7 +163,7 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing)', path: createCDNPath('bundle.tracing.min.js'), gzip: true, - limit: '42 KB', + limit: '42.1 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay)', @@ -231,7 +231,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '51 KB', + limit: '51.1 KB', }, // Node SDK (ESM) { diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 1c925d930036..b7e0cab509c1 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -11,13 +11,14 @@ import { _INTERNAL_flushMetricsBuffer } from './metrics/internal'; import type { Scope } from './scope'; import { updateSession } from './session'; import { getDynamicSamplingContextFromScope } from './tracing/dynamicSamplingContext'; +import { DEFAULT_TRANSPORT_BUFFER_SIZE } from './transports/base'; import type { Breadcrumb, BreadcrumbHint, FetchBreadcrumbHint, XhrBreadcrumbHint } from './types-hoist/breadcrumb'; import type { CheckIn, MonitorConfig } from './types-hoist/checkin'; import type { EventDropReason, Outcome } from './types-hoist/clientreport'; import type { DataCategory } from './types-hoist/datacategory'; import type { DsnComponents } from './types-hoist/dsn'; import type { DynamicSamplingContext, Envelope } from './types-hoist/envelope'; -import type { ErrorEvent, Event, EventHint, TransactionEvent } from './types-hoist/event'; +import type { ErrorEvent, Event, EventHint, EventType, TransactionEvent } from './types-hoist/event'; import type { EventProcessor } from './types-hoist/eventprocessor'; import type { FeedbackEvent } from './types-hoist/feedback'; import type { Integration } from './types-hoist/integration'; @@ -43,6 +44,7 @@ import { merge } from './utils/merge'; import { checkOrSetAlreadyCaught, uuid4 } from './utils/misc'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; +import { type PromiseBuffer, makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from './utils/promisebuffer'; import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span'; import { showSpanDropWarning } from './utils/spanUtils'; import { rejectedSyncPromise } from './utils/syncpromise'; @@ -201,6 +203,8 @@ export abstract class Client { // eslint-disable-next-line @typescript-eslint/ban-types private _hooks: Record>; + private _promiseBuffer: PromiseBuffer; + /** * Initializes this client instance. * @@ -213,6 +217,7 @@ export abstract class Client { this._outcomes = {}; this._hooks = {}; this._eventProcessors = []; + this._promiseBuffer = makePromiseBuffer(options.transportOptions?.bufferSize ?? DEFAULT_TRANSPORT_BUFFER_SIZE); if (options.dsn) { this._dsn = makeDsn(options.dsn); @@ -275,9 +280,11 @@ export abstract class Client { }; this._process( - this.eventFromException(exception, hintWithEventId).then(event => - this._captureEvent(event, hintWithEventId, scope), - ), + () => + this.eventFromException(exception, hintWithEventId) + .then(event => this._captureEvent(event, hintWithEventId, scope)) + .then(res => res), + 'error', ); return hintWithEventId.event_id; @@ -300,12 +307,15 @@ export abstract class Client { }; const eventMessage = isParameterizedString(message) ? message : String(message); - - const promisedEvent = isPrimitive(message) + const isMessage = isPrimitive(message); + const promisedEvent = isMessage ? this.eventFromMessage(eventMessage, level, hintWithEventId) : this.eventFromException(message, hintWithEventId); - this._process(promisedEvent.then(event => this._captureEvent(event, hintWithEventId, currentScope))); + this._process( + () => promisedEvent.then(event => this._captureEvent(event, hintWithEventId, currentScope)), + isMessage ? 'unknown' : 'error', + ); return hintWithEventId.event_id; } @@ -332,9 +342,11 @@ export abstract class Client { const sdkProcessingMetadata = event.sdkProcessingMetadata || {}; const capturedSpanScope: Scope | undefined = sdkProcessingMetadata.capturedSpanScope; const capturedSpanIsolationScope: Scope | undefined = sdkProcessingMetadata.capturedSpanIsolationScope; + const dataCategory = getDataCategoryByType(event.type); this._process( - this._captureEvent(event, hintWithEventId, capturedSpanScope || currentScope, capturedSpanIsolationScope), + () => this._captureEvent(event, hintWithEventId, capturedSpanScope || currentScope, capturedSpanIsolationScope), + dataCategory, ); return hintWithEventId.event_id; @@ -1252,7 +1264,7 @@ export abstract class Client { ); } - const dataCategory = (eventType === 'replay_event' ? 'replay' : eventType) satisfies DataCategory; + const dataCategory = getDataCategoryByType(event.type); return this._prepareEvent(event, hint, currentScope, isolationScope) .then(prepared => { @@ -1335,15 +1347,21 @@ export abstract class Client { /** * Occupies the client with processing and event */ - protected _process(promise: PromiseLike): void { + protected _process(taskProducer: () => PromiseLike, dataCategory: DataCategory): void { this._numProcessing++; - void promise.then( + + void this._promiseBuffer.add(taskProducer).then( value => { this._numProcessing--; return value; }, reason => { this._numProcessing--; + + if (reason === SENTRY_BUFFER_FULL_ERROR) { + this.recordDroppedEvent('queue_overflow', dataCategory); + } + return reason; }, ); @@ -1408,6 +1426,10 @@ export abstract class Client { ): PromiseLike; } +function getDataCategoryByType(type: EventType | 'replay_event' | undefined): DataCategory { + return type === 'replay_event' ? 'replay' : type || 'error'; +} + /** * Verifies that return value of configured `beforeSend` or `beforeSendTransaction` is of expected type, and returns the value if so. */ diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index c009d0e0c2a8..19ef8a95dff5 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -15,6 +15,7 @@ import { import * as integrationModule from '../../src/integration'; import { _INTERNAL_captureLog } from '../../src/logs/internal'; import { _INTERNAL_captureMetric } from '../../src/metrics/internal'; +import { DEFAULT_TRANSPORT_BUFFER_SIZE } from '../../src/transports/base'; import type { Envelope } from '../../src/types-hoist/envelope'; import type { ErrorEvent, Event, TransactionEvent } from '../../src/types-hoist/event'; import type { SpanJSON } from '../../src/types-hoist/span'; @@ -23,7 +24,7 @@ import * as miscModule from '../../src/utils/misc'; import * as stringModule from '../../src/utils/string'; import * as timeModule from '../../src/utils/time'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; -import { AdHocIntegration, TestIntegration } from '../mocks/integration'; +import { AdHocIntegration, AsyncTestIntegration, TestIntegration } from '../mocks/integration'; import { makeFakeTransport } from '../mocks/transport'; import { clearGlobalScope } from '../testutils'; @@ -2935,4 +2936,66 @@ describe('Client', () => { expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); }); }); + + describe('promise buffer usage', () => { + it('respects the default value of the buffer size', async () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + + client.addIntegration(new AsyncTestIntegration()); + + Array.from({ length: DEFAULT_TRANSPORT_BUFFER_SIZE + 1 }).forEach(() => { + client.captureException(new Error('ʕノ•ᴥ•ʔノ ︵ ┻━┻')); + }); + + expect(client._clearOutcomes()).toEqual([{ reason: 'queue_overflow', category: 'error', quantity: 1 }]); + }); + + it('records queue_overflow when promise buffer is full', async () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, transportOptions: { bufferSize: 1 } }); + const client = new TestClient(options); + + client.addIntegration(new AsyncTestIntegration()); + + client.captureException(new Error('first')); + client.captureException(new Error('second')); + client.captureException(new Error('third')); + + expect(client._clearOutcomes()).toEqual([{ reason: 'queue_overflow', category: 'error', quantity: 2 }]); + }); + + it('records different types of dropped events', async () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, transportOptions: { bufferSize: 1 } }); + const client = new TestClient(options); + + client.addIntegration(new AsyncTestIntegration()); + + client.captureException(new Error('first')); // error + client.captureException(new Error('second')); // error + client.captureMessage('third'); // unknown + client.captureEvent({ message: 'fourth' }); // error + client.captureEvent({ message: 'fifth', type: 'replay_event' }); // replay + client.captureEvent({ message: 'sixth', type: 'transaction' }); // transaction + + expect(client._clearOutcomes()).toEqual([ + { reason: 'queue_overflow', category: 'error', quantity: 2 }, + { reason: 'queue_overflow', category: 'unknown', quantity: 1 }, + { reason: 'queue_overflow', category: 'replay', quantity: 1 }, + { reason: 'queue_overflow', category: 'transaction', quantity: 1 }, + ]); + }); + + it('should skip the promise buffer with sync integrations', async () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, transportOptions: { bufferSize: 1 } }); + const client = new TestClient(options); + + client.addIntegration(new TestIntegration()); + + client.captureException(new Error('first')); + client.captureException(new Error('second')); + client.captureException(new Error('third')); + + expect(client._clearOutcomes()).toEqual([]); + }); + }); }); diff --git a/packages/core/test/mocks/integration.ts b/packages/core/test/mocks/integration.ts index 72a18dabe7b3..f5fc5682265a 100644 --- a/packages/core/test/mocks/integration.ts +++ b/packages/core/test/mocks/integration.ts @@ -24,6 +24,16 @@ export class TestIntegration implements Integration { } } +export class AsyncTestIntegration implements Integration { + public static id: string = 'AsyncTestIntegration'; + + public name: string = 'AsyncTestIntegration'; + + processEvent(event: Event): Event | null | PromiseLike { + return new Promise(resolve => setTimeout(() => resolve(event), 1)); + } +} + export class AddAttachmentTestIntegration implements Integration { public static id: string = 'AddAttachmentTestIntegration'; From 67095c8508e8d546f6e762fea161d1b8039e6c74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:22:03 +0100 Subject: [PATCH 15/32] feat(deps): bump @sentry/cli from 2.56.0 to 2.58.2 (#18271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@sentry/cli](https://github.com/getsentry/sentry-cli) from 2.56.0 to 2.58.2.
Release notes

Sourced from @​sentry/cli's releases.

2.58.2

Improvements

  • Added validation for the sentry-cli build upload command's --head-sha and --base-sha arguments (#2945). The CLI now validates that these are valid SHA1 sums. Passing an empty string is also allowed; this prevents the default values from being used, causing the values to instead be unset.

Fixes

  • Fixed a bug where providing empty-string values for the sentry-cli build upload command's --vcs-provider, --head-repo-name, --head-ref, --base-ref, and --base-repo-name arguments resulted in 400 errors (#2946). Now, setting these to empty strings instead explicitly clears the default value we would set otherwise, as expected.

2.58.1

Deprecations

  • Deprecated API key authentication (#2934, #2937). Users who are still using API keys to authenticate Sentry CLI should generate and use an Auth Token instead.

Improvements

  • The sentry-cli debug-files bundle-jvm no longer makes any HTTP requests to Sentry, meaning auth tokens are no longer needed, and the command can be run offline (#2926).

Fixes

  • Skip setting base_sha and base_ref when they equal head_sha during auto-inference, since comparing a commit to itself provides no meaningful baseline (#2924).
  • Improved error message when supplying a non-existent organization to sentry-cli sourcemaps upload. The error now correctly indicates the organization doesn't exist, rather than incorrectly suggesting the Sentry server lacks artifact bundle support (#2931).

2.58.0

New Features

  • Removed experimental status from the sentry-cli build upload commands (#2899, #2905). At the time of this release, build uploads are still in closed beta on the server side, so most customers cannot use this functionality quite yet.
  • Added CLI version metadata to build upload archives (#2890).

Deprecations

  • Deprecated the upload-proguard subcommand's --platform flag (#2863). This flag was a no-op for some time, so we will remove it in the next major.
  • Deprecated the upload-proguard subcommand's --android-manifest flag (#2891). This flag was a no-op for some time, so we will remove it in the next major.
  • Deprecated the sentry-cli sourcemaps upload command's --no-dedupe flag (#2913). The flag was no longer relevant for sourcemap uploads to modern Sentry servers and was made a no-op.

Fixes

  • Fixed autofilled git base metadata (--base-ref, --base-sha) when using the build upload subcommand in git repos. Previously this worked only in the context of GitHub workflows (#2897, #2898).

Performance

  • Slightly sped up the sentry-cli sourcemaps upload command by eliminating an HTTP request to the Sentry server, which was not required in most cases (#2913).

2.57.0

New Features

  • (JS API) Add projects field to SentryCliUploadSourceMapsOptions (#2856)

Deprecations

... (truncated)

Changelog

Sourced from @​sentry/cli's changelog.

2.58.2

Improvements

  • Added validation for the sentry-cli build upload command's --head-sha and --base-sha arguments (#2945). The CLI now validates that these are valid SHA1 sums. Passing an empty string is also allowed; this prevents the default values from being used, causing the values to instead be unset.

Fixes

  • Fixed a bug where providing empty-string values for the sentry-cli build upload command's --vcs-provider, --head-repo-name, --head-ref, --base-ref, and --base-repo-name arguments resulted in 400 errors (#2946). Now, setting these to empty strings instead explicitly clears the default value we would set otherwise, as expected.

2.58.1

Deprecations

  • Deprecated API key authentication (#2934, #2937). Users who are still using API keys to authenticate Sentry CLI should generate and use an Auth Token instead.

Improvements

  • The sentry-cli debug-files bundle-jvm no longer makes any HTTP requests to Sentry, meaning auth tokens are no longer needed, and the command can be run offline (#2926).

Fixes

  • Skip setting base_sha and base_ref when they equal head_sha during auto-inference, since comparing a commit to itself provides no meaningful baseline (#2924).
  • Improved error message when supplying a non-existent organization to sentry-cli sourcemaps upload. The error now correctly indicates the organization doesn't exist, rather than incorrectly suggesting the Sentry server lacks artifact bundle support (#2931).

2.58.0

New Features

  • Removed experimental status from the sentry-cli build upload commands (#2899, #2905). At the time of this release, build uploads are still in closed beta on the server side, so most customers cannot use this functionality quite yet.
  • Added CLI version metadata to build upload archives (#2890).

Deprecations

  • Deprecated the upload-proguard subcommand's --platform flag (#2863). This flag was a no-op for some time, so we will remove it in the next major.
  • Deprecated the upload-proguard subcommand's --android-manifest flag (#2891). This flag was a no-op for some time, so we will remove it in the next major.
  • Deprecated the sentry-cli sourcemaps upload command's --no-dedupe flag (#2913). The flag was no longer relevant for sourcemap uploads to modern Sentry servers and was made a no-op.

Fixes

  • Fixed autofilled git base metadata (--base-ref, --base-sha) when using the build upload subcommand in git repos. Previously this worked only in the context of GitHub workflows (#2897, #2898).

Performance

  • Slightly sped up the sentry-cli sourcemaps upload command by eliminating an HTTP request to the Sentry server, which was not required in most cases (#2913).

Internal changes

  • Migrated JavaScript wrapper to TypeScript for better type safety (#2910)

... (truncated)

Commits
  • b8965a3 release: 2.58.2
  • f99509f fix(build): Allow clearing string arguments to build upload (#2946)
  • a2cef20 ref(build): Add client-side validation for SHA fields (#2945)
  • c550aa7 ref(build): Move VcsInfo beside other build upload API types (#2944)
  • f303fd4 ref(build): Use VcsInfo directly in ChunkedBuildRequest (#2943)
  • 63b187c meta(cargo): Remove authors from Cargo.toml (#2939)
  • 1ccff9d build(npm): 🤖 Bump optional dependencies to 2.58.1
  • 4362cf6 Merge branch 'release/2.58.1'
  • b25423a release: 2.58.1
  • 7595ba9 chore(js): Deprecate apiKey field (#2937)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sentry/cli&package-manager=npm_and_yarn&previous-version=2.56.0&new-version=2.58.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/react-router/package.json | 2 +- packages/remix/package.json | 2 +- yarn.lock | 105 +++++++++++++++-------------- 3 files changed, 55 insertions(+), 54 deletions(-) diff --git a/packages/react-router/package.json b/packages/react-router/package.json index d988e14aea8a..a65bd845bdab 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -50,7 +50,7 @@ "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@sentry/browser": "10.26.0", - "@sentry/cli": "^2.56.0", + "@sentry/cli": "^2.58.2", "@sentry/core": "10.26.0", "@sentry/node": "10.26.0", "@sentry/react": "10.26.0", diff --git a/packages/remix/package.json b/packages/remix/package.json index 667f77d53648..181d6e23a63f 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -68,7 +68,7 @@ "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@remix-run/router": "1.x", - "@sentry/cli": "^2.56.0", + "@sentry/cli": "^2.58.2", "@sentry/core": "10.26.0", "@sentry/node": "10.26.0", "@sentry/react": "10.26.0", diff --git a/yarn.lock b/yarn.lock index 93c288553b5b..e3f84550ba9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7103,50 +7103,50 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/cli-darwin@2.56.0": - version "2.56.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.56.0.tgz#53fa7de2c26f6450d5454ba997c26c2471d112c8" - integrity sha512-CzXFWbv3GrjU0gFlUM9jt0fvJmyo5ktty4HGxRFfS/eMC6xW58Gg/sEeMVEkdvk5osKooX/YEgfLBdo4zvuWDA== - -"@sentry/cli-linux-arm64@2.56.0": - version "2.56.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.56.0.tgz#5041c8877416a607ddae87b948cbe6c9e86d7f54" - integrity sha512-91d5ZlC989j/t+TXor/glPyx6SnLFS/SlJ9fIrHIQohdGKyWWSFb4VKUan8Ok3GYu9SUzKTMByryIOoYEmeGVw== - -"@sentry/cli-linux-arm@2.56.0": - version "2.56.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.56.0.tgz#c7875cf5f76e254ff1c0f49cf99d8c26b6ec4959" - integrity sha512-vQCCMhZLugPmr25XBoP94dpQsFa110qK5SBUVJcRpJKyzMZd+6ueeHNslq2mB0OF4BwL1qd/ZDIa4nxa1+0rjQ== - -"@sentry/cli-linux-i686@2.56.0": - version "2.56.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.56.0.tgz#aeaff32f9f0d405e413373223e406d66b1d56176" - integrity sha512-MZzXuq1Q/TktN81DUs6XSBU752pG3XWSJdZR+NCStIg3l8s3O/Pwh6OcDHTYqgwsYJaGBpA0fP2Afl5XeSAUNg== - -"@sentry/cli-linux-x64@2.56.0": - version "2.56.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.56.0.tgz#3dd4ef83c2d710c3e6f5d078d05391fda2ce23ee" - integrity sha512-INOO2OQ90Y3UzYgHRdrHdKC/0es3YSHLv0iNNgQwllL0YZihSVNYSSrZqcPq8oSDllEy9Vt9oOm/7qEnUP2Kfw== - -"@sentry/cli-win32-arm64@2.56.0": - version "2.56.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.56.0.tgz#2113bcac721970ca4dbd04a6dab37dfb0ec147d2" - integrity sha512-eUvkVk9KK01q6/qyugQPh7dAxqFPbgOa62QAoSwo11WQFYc3NPgJLilFWLQo+nahHGYKh6PKuCJ5tcqnQq5Hkg== - -"@sentry/cli-win32-i686@2.56.0": - version "2.56.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.56.0.tgz#bd8e646f4b5a98aa80bc9751a6e0db6514a935f5" - integrity sha512-mpCA8hKXuvT17bl1H/54KOa5i+02VBBHVlOiP3ltyBuQUqfvX/30Zl/86Spy+ikodovZWAHv5e5FpyXbY1/mPw== - -"@sentry/cli-win32-x64@2.56.0": - version "2.56.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.56.0.tgz#1acc7ca166ed531075a31b2bc1700294747da6b8" - integrity sha512-UV0pXNls+/ViAU/3XsHLLNEHCsRYaGEwJdY3HyGIufSlglxrX6BVApkV9ziGi4WAxcJWLjQdfcEs6V5B+wBy0A== - -"@sentry/cli@^2.51.0", "@sentry/cli@^2.56.0": - version "2.56.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.56.0.tgz#13dc043c78687b47285cc45db5bcfb65bbdb6dd9" - integrity sha512-br6+1nTPUV5EG1oaxLzxv31kREFKr49Y1+3jutfMUz9Nl8VyVP7o9YwakB/YWl+0Vi0NXg5vq7qsd/OOuV5j8w== +"@sentry/cli-darwin@2.58.2": + version "2.58.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.58.2.tgz#61f6f836de8ac2e1992ccadc0368bc403f23c609" + integrity sha512-MArsb3zLhA2/cbd4rTm09SmTpnEuZCoZOpuZYkrpDw1qzBVJmRFA1W1hGAQ9puzBIk/ubY3EUhhzuU3zN2uD6w== + +"@sentry/cli-linux-arm64@2.58.2": + version "2.58.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.2.tgz#3a7a9c83e31b482599ce08d93d5ba6c8a1a44c7f" + integrity sha512-ay3OeObnbbPrt45cjeUyQjsx5ain1laj1tRszWj37NkKu55NZSp4QCg1gGBZ0gBGhckI9nInEsmKtix00alw2g== + +"@sentry/cli-linux-arm@2.58.2": + version "2.58.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.2.tgz#f9bef6802cb707d1603a02e0727fed22d834e133" + integrity sha512-HU9lTCzcHqCz/7Mt5n+cv+nFuJdc1hGD2h35Uo92GgxX3/IujNvOUfF+nMX9j6BXH6hUt73R5c0Ycq9+a3Parg== + +"@sentry/cli-linux-i686@2.58.2": + version "2.58.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.2.tgz#a3e6cb24d314f2d948b96457731f9345dc8370f9" + integrity sha512-CN9p0nfDFsAT1tTGBbzOUGkIllwS3hygOUyTK7LIm9z+UHw5uNgNVqdM/3Vg+02ymjkjISNB3/+mqEM5osGXdA== + +"@sentry/cli-linux-x64@2.58.2": + version "2.58.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.2.tgz#8e071e11b03524b08d369075f3203b05529ca233" + integrity sha512-oX/LLfvWaJO50oBVOn4ZvG2SDWPq0MN8SV9eg5tt2nviq+Ryltfr7Rtoo+HfV+eyOlx1/ZXhq9Wm7OT3cQuz+A== + +"@sentry/cli-win32-arm64@2.58.2": + version "2.58.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.2.tgz#af109a165c25245458a6c58b79a91c639b1df1b0" + integrity sha512-+cl3x2HPVMpoSVGVM1IDWlAEREZrrVQj4xBb0TRKII7g3hUxRsAIcsrr7+tSkie++0FuH4go/b5fGAv51OEF3w== + +"@sentry/cli-win32-i686@2.58.2": + version "2.58.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.2.tgz#53038b43b2c14c419fb71586f7448e7580ed4e39" + integrity sha512-omFVr0FhzJ8oTJSg1Kf+gjLgzpYklY0XPfLxZ5iiMiYUKwF5uo1RJRdkUOiEAv0IqpUKnmKcmVCLaDxsWclB7Q== + +"@sentry/cli-win32-x64@2.58.2": + version "2.58.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.2.tgz#b4c81a3c163344ae8b27523a0391e7f99c533f41" + integrity sha512-2NAFs9UxVbRztQbgJSP5i8TB9eJQ7xraciwj/93djrSMHSEbJ0vC47TME0iifgvhlHMs5vqETOKJtfbbpQAQFA== + +"@sentry/cli@^2.51.0", "@sentry/cli@^2.58.2": + version "2.58.2" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.58.2.tgz#0d6e19a1771d27aae8b2765a6f3e96062e2c7502" + integrity sha512-U4u62V4vaTWF+o40Mih8aOpQKqKUbZQt9A3LorIJwaE3tO3XFLRI70eWtW2se1Qmy0RZ74zB14nYcFNFl2t4Rw== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -7154,14 +7154,14 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.56.0" - "@sentry/cli-linux-arm" "2.56.0" - "@sentry/cli-linux-arm64" "2.56.0" - "@sentry/cli-linux-i686" "2.56.0" - "@sentry/cli-linux-x64" "2.56.0" - "@sentry/cli-win32-arm64" "2.56.0" - "@sentry/cli-win32-i686" "2.56.0" - "@sentry/cli-win32-x64" "2.56.0" + "@sentry/cli-darwin" "2.58.2" + "@sentry/cli-linux-arm" "2.58.2" + "@sentry/cli-linux-arm64" "2.58.2" + "@sentry/cli-linux-i686" "2.58.2" + "@sentry/cli-linux-x64" "2.58.2" + "@sentry/cli-win32-arm64" "2.58.2" + "@sentry/cli-win32-i686" "2.58.2" + "@sentry/cli-win32-x64" "2.58.2" "@sentry/rollup-plugin@^4.3.0": version "4.3.0" @@ -28836,6 +28836,7 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" + uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" From 1a6be80e573ebe27226eb92fb4125390155c9db4 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 21 Nov 2025 10:49:25 +0100 Subject: [PATCH 16/32] chore(angular): Add Angular 21 Support (#18274) Angular 21 was [released](https://www.npmjs.com/package/@angular/cli) silently yesterday. Migration docs don't seem to indicate any breaking change for us. --- .../angular-21/.editorconfig | 17 + .../test-applications/angular-21/.gitignore | 44 +++ .../test-applications/angular-21/.npmrc | 2 + .../test-applications/angular-21/README.md | 3 + .../test-applications/angular-21/angular.json | 87 +++++ .../test-applications/angular-21/package.json | 60 ++++ .../angular-21/playwright.config.mjs | 8 + .../angular-21/public/favicon.ico | Bin 0 -> 15086 bytes .../angular-21/src/app/app.component.ts | 12 + .../angular-21/src/app/app.config.ts | 29 ++ .../angular-21/src/app/app.routes.ts | 42 +++ .../angular-21/src/app/cancel-guard.guard.ts | 5 + .../src/app/cancel/cancel.components.ts | 8 + .../component-tracking.components.ts | 21 ++ .../angular-21/src/app/home/home.component.ts | 26 ++ .../sample-component.components.ts | 12 + .../angular-21/src/app/user/user.component.ts | 25 ++ .../angular-21/src/index.html | 13 + .../test-applications/angular-21/src/main.ts | 15 + .../angular-21/src/styles.css | 1 + .../angular-21/start-event-proxy.mjs | 6 + .../angular-21/tests/errors.test.ts | 65 ++++ .../angular-21/tests/performance.test.ts | 327 ++++++++++++++++++ .../angular-21/tsconfig.app.json | 11 + .../angular-21/tsconfig.json | 27 ++ .../angular-21/tsconfig.spec.json | 10 + packages/angular/package.json | 6 +- 27 files changed, 879 insertions(+), 3 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/.editorconfig create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/README.md create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/angular.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/package.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/app/app.component.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/app/app.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/app/app.routes.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/app/cancel-guard.guard.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/app/cancel/cancel.components.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/app/component-tracking/component-tracking.components.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/app/home/home.component.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/app/sample-component/sample-component.components.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/app/user/user.component.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/index.html create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/main.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/src/styles.css create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/tsconfig.app.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-21/tsconfig.spec.json diff --git a/dev-packages/e2e-tests/test-applications/angular-21/.editorconfig b/dev-packages/e2e-tests/test-applications/angular-21/.editorconfig new file mode 100644 index 000000000000..f166060da1cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/dev-packages/e2e-tests/test-applications/angular-21/.gitignore b/dev-packages/e2e-tests/test-applications/angular-21/.gitignore new file mode 100644 index 000000000000..315c644a53e8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db + +test-results diff --git a/dev-packages/e2e-tests/test-applications/angular-21/.npmrc b/dev-packages/e2e-tests/test-applications/angular-21/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/.npmrc @@ -0,0 +1,2 @@ +@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/angular-21/README.md b/dev-packages/e2e-tests/test-applications/angular-21/README.md new file mode 100644 index 000000000000..6d3a1ff489df --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/README.md @@ -0,0 +1,3 @@ +# Angular 21 + +E2E test app for Angular 21 and `@sentry/angular`. diff --git a/dev-packages/e2e-tests/test-applications/angular-21/angular.json b/dev-packages/e2e-tests/test-applications/angular-21/angular.json new file mode 100644 index 000000000000..18bf58596766 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/angular.json @@ -0,0 +1,87 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "angular-21": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/angular-21", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "angular-21:build:production" + }, + "development": { + "buildTarget": "angular-21:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"], + "scripts": [] + } + } + } + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-21/package.json b/dev-packages/e2e-tests/test-applications/angular-21/package.json new file mode 100644 index 000000000000..315f7eea0492 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/package.json @@ -0,0 +1,60 @@ +{ + "name": "angular-21", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "dev": "ng serve", + "proxy": "node start-event-proxy.mjs", + "preview": "http-server dist/angular-21/browser --port 8080 --silent", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:build-canary": "pnpm install && pnpm add @angular/animations@next @angular/common@next @angular/compiler@next @angular/core@next @angular/forms@next @angular/platform-browser@next @angular/platform-browser-dynamic@next @angular/router@next && pnpm add -D @angular-devkit/build-angular@next @angular/cli@next @angular/compiler-cli@next && pnpm build", + "test:assert": "playwright test", + "clean": "npx rimraf .angular node_modules pnpm-lock.yaml dist" + }, + "private": true, + "dependencies": { + "@angular/animations": "^21.0.0", + "@angular/common": "^21.0.0", + "@angular/compiler": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/forms": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-browser-dynamic": "^21.0.0", + "@angular/router": "^21.0.0", + "@sentry/angular": "* || latest", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^21.0.0", + "@angular/cli": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@playwright/test": "~1.53.2", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/core": "latest || *", + "@types/jasmine": "~5.1.0", + "http-server": "^14.1.1", + "jasmine-core": "~5.4.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.9.0" + }, + "volta": { + "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "build-command": "pnpm test:build-canary", + "label": "angular (canary)" + } + ] + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-21/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/angular-21/playwright.config.mjs new file mode 100644 index 000000000000..0845325879c9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm preview`, + port: 8080, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/angular-21/public/favicon.ico b/dev-packages/e2e-tests/test-applications/angular-21/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/app/app.component.ts b/dev-packages/e2e-tests/test-applications/angular-21/src/app/app.component.ts new file mode 100644 index 000000000000..90cd343e9449 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/app/app.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + template: ``, +}) +export class AppComponent { + title = 'angular-21'; +} diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/app/app.config.ts b/dev-packages/e2e-tests/test-applications/angular-21/src/app/app.config.ts new file mode 100644 index 000000000000..f5cc30f3615b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/app/app.config.ts @@ -0,0 +1,29 @@ +import { + ApplicationConfig, + ErrorHandler, + inject, + provideAppInitializer, + provideZoneChangeDetection, +} from '@angular/core'; +import { Router, provideRouter } from '@angular/router'; + +import { TraceService, createErrorHandler } from '@sentry/angular'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + { + provide: ErrorHandler, + useValue: createErrorHandler(), + }, + { + provide: TraceService, + deps: [Router], + }, + provideAppInitializer(() => { + inject(TraceService); + }), + ], +}; diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/app/app.routes.ts b/dev-packages/e2e-tests/test-applications/angular-21/src/app/app.routes.ts new file mode 100644 index 000000000000..24bf8b769051 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/app/app.routes.ts @@ -0,0 +1,42 @@ +import { Routes } from '@angular/router'; +import { cancelGuard } from './cancel-guard.guard'; +import { CancelComponent } from './cancel/cancel.components'; +import { ComponentTrackingComponent } from './component-tracking/component-tracking.components'; +import { HomeComponent } from './home/home.component'; +import { UserComponent } from './user/user.component'; + +export const routes: Routes = [ + { + path: 'users/:id', + component: UserComponent, + }, + { + path: 'home', + component: HomeComponent, + }, + { + path: 'cancel', + component: CancelComponent, + canActivate: [cancelGuard], + }, + { + path: 'component-tracking', + component: ComponentTrackingComponent, + }, + { + path: 'redirect1', + redirectTo: '/redirect2', + }, + { + path: 'redirect2', + redirectTo: '/redirect3', + }, + { + path: 'redirect3', + redirectTo: '/users/456', + }, + { + path: '**', + redirectTo: 'home', + }, +]; diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/app/cancel-guard.guard.ts b/dev-packages/e2e-tests/test-applications/angular-21/src/app/cancel-guard.guard.ts new file mode 100644 index 000000000000..16ec4a2ab164 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/app/cancel-guard.guard.ts @@ -0,0 +1,5 @@ +import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; + +export const cancelGuard: CanActivateFn = (_next: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { + return false; +}; diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/app/cancel/cancel.components.ts b/dev-packages/e2e-tests/test-applications/angular-21/src/app/cancel/cancel.components.ts new file mode 100644 index 000000000000..b6ee1876e035 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/app/cancel/cancel.components.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-cancel', + standalone: true, + template: `
`, +}) +export class CancelComponent {} diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/app/component-tracking/component-tracking.components.ts b/dev-packages/e2e-tests/test-applications/angular-21/src/app/component-tracking/component-tracking.components.ts new file mode 100644 index 000000000000..76bd580ecaf6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/app/component-tracking/component-tracking.components.ts @@ -0,0 +1,21 @@ +import { AfterViewInit, Component, OnInit } from '@angular/core'; +import { TraceClass, TraceMethod, TraceModule } from '@sentry/angular'; +import { SampleComponent } from '../sample-component/sample-component.components'; + +@Component({ + selector: 'app-component-tracking', + standalone: true, + imports: [TraceModule, SampleComponent], + template: ` + + + `, +}) +@TraceClass({ name: 'ComponentTrackingComponent' }) +export class ComponentTrackingComponent implements OnInit, AfterViewInit { + @TraceMethod({ name: 'ngOnInit' }) + ngOnInit() {} + + @TraceMethod() + ngAfterViewInit() {} +} diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/app/home/home.component.ts b/dev-packages/e2e-tests/test-applications/angular-21/src/app/home/home.component.ts new file mode 100644 index 000000000000..78b914602eb9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/app/home/home.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-home', + standalone: true, + imports: [RouterLink], + template: ` +
+

Welcome to Sentry's Angular 21 E2E test app

+ + +
+ `, +}) +export class HomeComponent { + throwError() { + throw new Error('Error thrown from Angular 21 E2E test app'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/app/sample-component/sample-component.components.ts b/dev-packages/e2e-tests/test-applications/angular-21/src/app/sample-component/sample-component.components.ts new file mode 100644 index 000000000000..da09425c7565 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/app/sample-component/sample-component.components.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-sample-component', + standalone: true, + template: `
Component
`, +}) +export class SampleComponent implements OnInit { + ngOnInit() { + console.log('SampleComponent'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/app/user/user.component.ts b/dev-packages/e2e-tests/test-applications/angular-21/src/app/user/user.component.ts new file mode 100644 index 000000000000..db02568d395f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/app/user/user.component.ts @@ -0,0 +1,25 @@ +import { AsyncPipe } from '@angular/common'; +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, map } from 'rxjs'; + +@Component({ + selector: 'app-user', + standalone: true, + imports: [AsyncPipe], + template: ` +

Hello User {{ userId$ | async }}

+ + `, +}) +export class UserComponent { + public userId$: Observable; + + constructor(private route: ActivatedRoute) { + this.userId$ = this.route.paramMap.pipe(map(params => params.get('id') || 'UNKNOWN USER')); + } + + throwError() { + throw new Error('Error thrown from user page'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/index.html b/dev-packages/e2e-tests/test-applications/angular-21/src/index.html new file mode 100644 index 000000000000..ffc9a3f96de6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/index.html @@ -0,0 +1,13 @@ + + + + + Angular 21 + + + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/main.ts b/dev-packages/e2e-tests/test-applications/angular-21/src/main.ts new file mode 100644 index 000000000000..a0b841afc333 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/main.ts @@ -0,0 +1,15 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; + +import * as Sentry from '@sentry/angular'; + +Sentry.init({ + // Cannot use process.env here, so we hardcode the DSN + dsn: 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', + tracesSampleRate: 1.0, + integrations: [Sentry.browserTracingIntegration({})], + tunnel: `http://localhost:3031/`, // proxy server +}); + +bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); diff --git a/dev-packages/e2e-tests/test-applications/angular-21/src/styles.css b/dev-packages/e2e-tests/test-applications/angular-21/src/styles.css new file mode 100644 index 000000000000..90d4ee0072ce --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/dev-packages/e2e-tests/test-applications/angular-21/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/angular-21/start-event-proxy.mjs new file mode 100644 index 000000000000..2ea1a8ef918c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'angular-21', +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-21/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/angular-21/tests/errors.test.ts new file mode 100644 index 000000000000..f4f219373104 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/tests/errors.test.ts @@ -0,0 +1,65 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends an error', async ({ page }) => { + const errorPromise = waitForError('angular-21', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/`); + + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Angular 21 E2E test app', + mechanism: { + type: 'auto.function.angular.error_handler', + handled: false, + }, + }, + ], + }, + transaction: '/home/', + }); +}); + +test('assigns the correct transaction value after a navigation', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const errorPromise = waitForError('angular-21', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + await page.waitForTimeout(5000); + + await page.locator('#navLink').click(); + + const [_, error] = await Promise.all([page.locator('#userErrorBtn').click(), errorPromise]); + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from user page', + mechanism: { + type: 'auto.function.angular.error_handler', + handled: false, + }, + }, + ], + }, + transaction: '/users/:id/', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts new file mode 100644 index 000000000000..cee1f939c4c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts @@ -0,0 +1,327 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +// Cannot use @sentry/angular here due to build stuff +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; + +test('sends a pageload transaction with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.angular', + }, + }, + transaction: '/home/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction with a parameterized URL', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + await page.waitForTimeout(5000); + + const [_, navigationTxn] = await Promise.all([page.locator('#navLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + }, + }, + transaction: '/users/:id/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction even if the pageload span is still active', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, pageloadTxn, navigationTxn] = await Promise.all([ + page.locator('#navLink').click(), + pageloadTxnPromise, + navigationTxnPromise, + ]); + + expect(pageloadTxn).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.angular', + }, + }, + transaction: '/home/', + transaction_info: { + source: 'route', + }, + }); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: '/users/:id/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('groups redirects within one navigation root span', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#redirectLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: '/users/:id/', + transaction_info: { + source: 'route', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe('/redirect1'); +}); + +test.describe('finish routing span', () => { + test('finishes routing span on navigation cancel', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#cancelLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: '/cancel', + transaction_info: { + source: 'url', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe('/cancel'); + }); + + test('finishes routing span on navigation error', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#nonExistentLink').click(), navigationTxnPromise]); + + const nonExistentRoute = '/non-existent'; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: nonExistentRoute, + transaction_info: { + source: 'url', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe(nonExistentRoute); + }); +}); + +test.describe('TraceDirective', () => { + test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const traceDirectiveSpans = navigationTxn.spans?.filter( + span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive', + ); + + 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), + }), + ]), + ); + }); +}); + +test.describe('TraceClass Decorator', () => { + test('adds init span for decorated class', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const classDecoratorSpan = navigationTxn.spans?.find( + span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_class_decorator', + ); + + expect(classDecoratorSpan).toBeDefined(); + expect(classDecoratorSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', + }, + description: '', + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_class_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); +}); + +test.describe('TraceMethod Decorator', () => { + test('adds name to span description of decorated method `ngOnInit`', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const ngInitSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.ngOnInit'); + + expect(ngInitSpan).toBeDefined(); + expect(ngInitSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.ngOnInit', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + }, + description: '', + op: 'ui.angular.ngOnInit', + origin: 'auto.ui.angular.trace_method_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); + + test('adds fallback name to span description of decorated method `ngAfterViewInit`', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-21', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const ngAfterViewInitSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.ngAfterViewInit'); + + expect(ngAfterViewInitSpan).toBeDefined(); + expect(ngAfterViewInitSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.ngAfterViewInit', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + }, + description: '', + op: 'ui.angular.ngAfterViewInit', + origin: 'auto.ui.angular.trace_method_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-21/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/angular-21/tsconfig.app.json new file mode 100644 index 000000000000..8886e903f8d0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/angular-21/tsconfig.json b/dev-packages/e2e-tests/test-applications/angular-21/tsconfig.json new file mode 100644 index 000000000000..5525117c6744 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/tsconfig.json @@ -0,0 +1,27 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-21/tsconfig.spec.json b/dev-packages/e2e-tests/test-applications/angular-21/tsconfig.spec.json new file mode 100644 index 000000000000..e00e30e6d4fb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-21/tsconfig.spec.json @@ -0,0 +1,10 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/packages/angular/package.json b/packages/angular/package.json index 01912cb13f79..fd378f4af2d8 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -15,9 +15,9 @@ "access": "public" }, "peerDependencies": { - "@angular/common": ">= 14.x <= 20.x", - "@angular/core": ">= 14.x <= 20.x", - "@angular/router": ">= 14.x <= 20.x", + "@angular/common": ">= 14.x <= 21.x", + "@angular/core": ">= 14.x <= 21.x", + "@angular/router": ">= 14.x <= 21.x", "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { From 108b027446cfb166a7dca04e69ec9207c2bf2406 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:57:05 +0100 Subject: [PATCH 17/32] feat(deps): bump @sentry/bundler-plugin-core from 4.3.0 to 4.6.1 (#18273) --- packages/nextjs/package.json | 2 +- yarn.lock | 31 +++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 15b6a2bf3040..9afdfec16d7b 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -80,7 +80,7 @@ "@opentelemetry/semantic-conventions": "^1.37.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "10.26.0", - "@sentry/bundler-plugin-core": "^4.3.0", + "@sentry/bundler-plugin-core": "^4.6.1", "@sentry/core": "10.26.0", "@sentry/node": "10.26.0", "@sentry/opentelemetry": "10.26.0", diff --git a/yarn.lock b/yarn.lock index e3f84550ba9e..8687df6cfa53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7089,7 +7089,12 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.3.0.tgz#c5b6cbb986952596d3ad233540a90a1fd18bad80" integrity sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw== -"@sentry/bundler-plugin-core@4.3.0", "@sentry/bundler-plugin-core@^4.3.0": +"@sentry/babel-plugin-component-annotate@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.1.tgz#94eec0293be8289daa574e18783e64d29203c236" + integrity sha512-aSIk0vgBqv7PhX6/Eov+vlI4puCE0bRXzUG5HdCsHBpAfeMkI8Hva6kSOusnzKqs8bf04hU7s3Sf0XxGTj/1AA== + +"@sentry/bundler-plugin-core@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.3.0.tgz#cf302522a3e5b8a3bf727635d0c6a7bece981460" integrity sha512-dmR4DJhJ4jqVWGWppuTL2blNFqOZZnt4aLkewbD1myFG3KVfUx8CrMQWEmGjkgPOtj5TO6xH9PyTJjXC6o5tnA== @@ -7103,6 +7108,20 @@ magic-string "0.30.8" unplugin "1.0.1" +"@sentry/bundler-plugin-core@^4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.1.tgz#d6013e6233bf663114f581bbd3c3a380ff9311d4" + integrity sha512-WPeRbnMXm927m4Kr69NTArPfI+p5/34FHftdCRI3LFPMyhZDzz6J3wLy4hzaVUgmMf10eLzmq2HGEMvpQmdynA== + dependencies: + "@babel/core" "^7.18.5" + "@sentry/babel-plugin-component-annotate" "4.6.1" + "@sentry/cli" "^2.57.0" + dotenv "^16.3.1" + find-up "^5.0.0" + glob "^10.5.0" + magic-string "0.30.8" + unplugin "1.0.1" + "@sentry/cli-darwin@2.58.2": version "2.58.2" resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.58.2.tgz#61f6f836de8ac2e1992ccadc0368bc403f23c609" @@ -7143,7 +7162,7 @@ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.2.tgz#b4c81a3c163344ae8b27523a0391e7f99c533f41" integrity sha512-2NAFs9UxVbRztQbgJSP5i8TB9eJQ7xraciwj/93djrSMHSEbJ0vC47TME0iifgvhlHMs5vqETOKJtfbbpQAQFA== -"@sentry/cli@^2.51.0", "@sentry/cli@^2.58.2": +"@sentry/cli@^2.51.0", "@sentry/cli@^2.57.0", "@sentry/cli@^2.58.2": version "2.58.2" resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.58.2.tgz#0d6e19a1771d27aae8b2765a6f3e96062e2c7502" integrity sha512-U4u62V4vaTWF+o40Mih8aOpQKqKUbZQt9A3LorIJwaE3tO3XFLRI70eWtW2se1Qmy0RZ74zB14nYcFNFl2t4Rw== @@ -17942,10 +17961,10 @@ glob@8.0.3: minimatch "^5.0.1" once "^1.3.0" -glob@^10.0.0, glob@^10.2.2, glob@^10.3.10, glob@^10.3.4, glob@^10.3.7, glob@^10.4.1, glob@^10.4.5: - version "10.4.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" - integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== +glob@^10.0.0, glob@^10.2.2, glob@^10.3.10, glob@^10.3.4, glob@^10.3.7, glob@^10.4.1, glob@^10.4.5, glob@^10.5.0: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== dependencies: foreground-child "^3.1.0" jackspeak "^3.1.2" From edc1f09dc7f8e6a67647f8c91aa00169319be4fb Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 21 Nov 2025 14:31:26 +0100 Subject: [PATCH 18/32] test(e2e): Fix astro config in test app (#18282) https://5-0-0-beta.docs.astro.build/en/guides/upgrade-to/v5/#removed-hybrid-rendering-mode the test app was bumped to v5 from dependabot in https://github.com/getsentry/sentry-javascript/pull/18259 --------- Co-authored-by: Andrei Borza --- .../test-applications/cloudflare-astro/astro.config.mjs | 1 - .../e2e-tests/test-applications/cloudflare-astro/package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/astro.config.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-astro/astro.config.mjs index 36414cf24b7c..026e6e4dac7c 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-astro/astro.config.mjs +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/astro.config.mjs @@ -6,7 +6,6 @@ const dsn = process.env.E2E_TEST_DSN; // https://astro.build/config export default defineConfig({ - output: 'hybrid', adapter: cloudflare({ imageService: 'passthrough', }), diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json index 4db15edabbd7..776cf271e86e 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json @@ -17,7 +17,7 @@ "test:assert": "pnpm -v" }, "dependencies": { - "@astrojs/cloudflare": "8.1.0", + "@astrojs/cloudflare": "12.6.11", "@sentry/astro": "latest || *", "astro": "5.15.9" }, From 3375de056e152cd58345a5b87b77a8a1068957bd Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 21 Nov 2025 15:42:21 +0100 Subject: [PATCH 19/32] feat(core): Add scope attribute APIs (#18165) This PR adds `scope.setAttribute`, `scope.setAttributes` and `scope.removeAttribute` methods, as specified in our [develop docs](https://develop.sentry.dev/sdk/telemetry/scopes/#setting-attributes). This intial PR only enables setting the attributes (including attributes with units) as well as the usual scope data operations (clone(), update(), clear(), getSpanData()). These attributes are not yet applied to any of the telemetry we eventually want them to apply to. I'll take care of this in a follow-up PR. closes https://github.com/getsentry/sentry-javascript/issues/18140 ref https://linear.app/getsentry/project/implement-global-attributes-api-javascript-02c3c74184fc/issues --------- Co-authored-by: Sigrid <32902192+s1gr1d@users.noreply.github.com> --- .size-limit.js | 14 +- packages/core/src/attributes.ts | 141 +++++++++++ packages/core/src/scope.ts | 96 +++++++- packages/core/test/lib/attributes.test.ts | 286 ++++++++++++++++++++++ packages/core/test/lib/scope.test.ts | 167 ++++++++++++- 5 files changed, 695 insertions(+), 9 deletions(-) create mode 100644 packages/core/src/attributes.ts create mode 100644 packages/core/test/lib/attributes.test.ts diff --git a/.size-limit.js b/.size-limit.js index 100444907e06..6e6ee0f68303 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -38,7 +38,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '41.5 KB', + limit: '42 KB', }, { name: '@sentry/browser (incl. Tracing, Profiling)', @@ -127,7 +127,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '43.5 KB', + limit: '44 KB', }, // Vue SDK (ESM) { @@ -142,7 +142,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '43.3 KB', + limit: '44 KB', }, // Svelte SDK (ESM) { @@ -163,7 +163,7 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing)', path: createCDNPath('bundle.tracing.min.js'), gzip: true, - limit: '42.1 KB', + limit: '42.5 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay)', @@ -183,14 +183,14 @@ module.exports = [ path: createCDNPath('bundle.min.js'), gzip: false, brotli: false, - limit: '80 KB', + limit: '82 KB', }, { name: 'CDN Bundle (incl. Tracing) - uncompressed', path: createCDNPath('bundle.tracing.min.js'), gzip: false, brotli: false, - limit: '125 KB', + limit: '127 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed', @@ -231,7 +231,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '51.1 KB', + limit: '52 KB', }, // Node SDK (ESM) { diff --git a/packages/core/src/attributes.ts b/packages/core/src/attributes.ts new file mode 100644 index 000000000000..d979d5c4350f --- /dev/null +++ b/packages/core/src/attributes.ts @@ -0,0 +1,141 @@ +import { DEBUG_BUILD } from './debug-build'; +import type { DurationUnit, FractionUnit, InformationUnit } from './types-hoist/measurement'; +import { debug } from './utils/debug-logger'; + +export type RawAttributes = T & ValidatedAttributes; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type RawAttribute = T extends { value: any } | { unit: any } ? AttributeObject : T; + +export type Attributes = Record; + +export type AttributeValueType = string | number | boolean | Array | Array | Array; + +type AttributeTypeMap = { + string: string; + integer: number; + double: number; + boolean: boolean; + 'string[]': Array; + 'integer[]': Array; + 'double[]': Array; + 'boolean[]': Array; +}; + +/* Generates a type from the AttributeTypeMap like: + | { value: string; type: 'string' } + | { value: number; type: 'integer' } + | { value: number; type: 'double' } + */ +type AttributeUnion = { + [K in keyof AttributeTypeMap]: { + value: AttributeTypeMap[K]; + type: K; + }; +}[keyof AttributeTypeMap]; + +export type TypedAttributeValue = AttributeUnion & { unit?: AttributeUnit }; + +export type AttributeObject = { + value: unknown; + unit?: AttributeUnit; +}; + +// Unfortunately, we loose type safety if we did something like Exclude +// so therefore we unionize between the three supported unit categories. +type AttributeUnit = DurationUnit | InformationUnit | FractionUnit; + +/* If an attribute has either a 'value' or 'unit' property, we use the ValidAttributeObject type. */ +export type ValidatedAttributes = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [K in keyof T]: T[K] extends { value: any } | { unit: any } ? AttributeObject : unknown; +}; + +/** + * Type-guard: The attribute object has the shape the official attribute object (value, type, unit). + * https://develop.sentry.dev/sdk/telemetry/scopes/#setting-attributes + */ +export function isAttributeObject(maybeObj: unknown): maybeObj is AttributeObject { + return ( + typeof maybeObj === 'object' && + maybeObj != null && + !Array.isArray(maybeObj) && + Object.keys(maybeObj).includes('value') + ); +} + +/** + * Converts an attribute value to a typed attribute value. + * + * Does not allow mixed arrays. In case of a mixed array, the value is stringified and the type is 'string'. + * All values besides the supported attribute types (see {@link AttributeTypeMap}) are stringified to a string attribute value. + * + * @param value - The value of the passed attribute. + * @returns The typed attribute. + */ +export function attributeValueToTypedAttributeValue(rawValue: unknown): TypedAttributeValue { + const { value, unit } = isAttributeObject(rawValue) ? rawValue : { value: rawValue, unit: undefined }; + return { ...getTypedAttributeValue(value), ...(unit && typeof unit === 'string' ? { unit } : {}) }; +} + +// Only allow string, boolean, or number types +const getPrimitiveType: ( + item: unknown, +) => keyof Pick | null = item => + typeof item === 'string' + ? 'string' + : typeof item === 'boolean' + ? 'boolean' + : typeof item === 'number' && !Number.isNaN(item) + ? Number.isInteger(item) + ? 'integer' + : 'double' + : null; + +function getTypedAttributeValue(value: unknown): TypedAttributeValue { + const primitiveType = getPrimitiveType(value); + if (primitiveType) { + // @ts-expect-error - TS complains because {@link TypedAttributeValue} is strictly typed to + // avoid setting the wrong `type` on the attribute value. + // In this case, getPrimitiveType already does the check but TS doesn't know that. + // The "clean" alternative is to return an object per `typeof value` case + // but that would require more bundle size + // Therefore, we ignore it. + return { value, type: primitiveType }; + } + + if (Array.isArray(value)) { + const coherentArrayType = value.reduce((acc: 'string' | 'boolean' | 'integer' | 'double' | null, item) => { + if (!acc || getPrimitiveType(item) !== acc) { + return null; + } + return acc; + }, getPrimitiveType(value[0])); + + if (coherentArrayType) { + return { value, type: `${coherentArrayType}[]` }; + } + } + + // Fallback: stringify the passed value + let fallbackValue = ''; + try { + fallbackValue = JSON.stringify(value) ?? String(value); + } catch { + try { + fallbackValue = String(value); + } catch { + DEBUG_BUILD && debug.warn('Failed to stringify attribute value', value); + // ignore + } + } + + // This is quite a low-quality message but we cannot safely log the original `value` + // here due to String() or JSON.stringify() potentially throwing. + DEBUG_BUILD && + debug.log(`Stringified attribute value to ${fallbackValue} because it's not a supported attribute value type`); + + return { + value: fallbackValue, + type: 'string', + }; +} diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index b23b01664431..2ec1f6480788 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -1,4 +1,5 @@ /* eslint-disable max-lines */ +import type { AttributeObject, RawAttribute, RawAttributes } from './attributes'; import type { Client } from './client'; import { DEBUG_BUILD } from './debug-build'; import { updateSession } from './session'; @@ -46,6 +47,7 @@ export interface ScopeContext { extra: Extras; contexts: Contexts; tags: { [key: string]: Primitive }; + attributes?: RawAttributes>; fingerprint: string[]; propagationContext: PropagationContext; } @@ -71,6 +73,8 @@ export interface ScopeData { breadcrumbs: Breadcrumb[]; user: User; tags: { [key: string]: Primitive }; + // TODO(v11): Make this a required field (could be subtly breaking if we did it today) + attributes?: RawAttributes>; extra: Extras; contexts: Contexts; attachments: Attachment[]; @@ -104,6 +108,9 @@ export class Scope { /** Tags */ protected _tags: { [key: string]: Primitive }; + /** Attributes */ + protected _attributes: RawAttributes>; + /** Extra */ protected _extra: Extras; @@ -155,6 +162,7 @@ export class Scope { this._attachments = []; this._user = {}; this._tags = {}; + this._attributes = {}; this._extra = {}; this._contexts = {}; this._sdkProcessingMetadata = {}; @@ -171,6 +179,7 @@ export class Scope { const newScope = new Scope(); newScope._breadcrumbs = [...this._breadcrumbs]; newScope._tags = { ...this._tags }; + newScope._attributes = { ...this._attributes }; newScope._extra = { ...this._extra }; newScope._contexts = { ...this._contexts }; if (this._contexts.flags) { @@ -294,6 +303,79 @@ export class Scope { return this.setTags({ [key]: value }); } + /** + * Sets attributes onto the scope. + * + * TODO: + * Currently, these attributes are not applied to any telemetry data but they will be in the future. + * + * @param newAttributes - The attributes to set on the scope. You can either pass in key-value pairs, or + * an object with a `value` and an optional `unit` (if applicable to your attribute). + * + * @example + * ```typescript + * scope.setAttributes({ + * is_admin: true, + * payment_selection: 'credit_card', + * clicked_products: [130, 554, 292], + * render_duration: { value: 'render_duration', unit: 'ms' }, + * }); + * ``` + */ + public setAttributes>(newAttributes: RawAttributes): this { + this._attributes = { + ...this._attributes, + ...newAttributes, + }; + + this._notifyScopeListeners(); + return this; + } + + /** + * Sets an attribute onto the scope. + * + * TODO: + * Currently, these attributes are not applied to any telemetry data but they will be in the future. + * + * @param key - The attribute key. + * @param value - the attribute value. You can either pass in a raw value, or an attribute + * object with a `value` and an optional `unit` (if applicable to your attribute). + * + * @example + * ```typescript + * scope.setAttribute('is_admin', true); + * scope.setAttribute('clicked_products', [130, 554, 292]); + * scope.setAttribute('render_duration', { value: 'render_duration', unit: 'ms' }); + * ``` + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public setAttribute extends { value: any } | { unit: any } ? AttributeObject : unknown>( + key: string, + value: RawAttribute, + ): this { + return this.setAttributes({ [key]: value }); + } + + /** + * Removes the attribute with the given key from the scope. + * + * @param key - The attribute key. + * + * @example + * ```typescript + * scope.removeAttribute('is_admin'); + * ``` + */ + public removeAttribute(key: string): this { + if (key in this._attributes) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this._attributes[key]; + this._notifyScopeListeners(); + } + return this; + } + /** * Set an object that will be merged into existing extra on the scope, * and will be sent as extra data with the event. @@ -409,9 +491,19 @@ export class Scope { ? (captureContext as ScopeContext) : undefined; - const { tags, extra, user, contexts, level, fingerprint = [], propagationContext } = scopeInstance || {}; + const { + tags, + attributes, + extra, + user, + contexts, + level, + fingerprint = [], + propagationContext, + } = scopeInstance || {}; this._tags = { ...this._tags, ...tags }; + this._attributes = { ...this._attributes, ...attributes }; this._extra = { ...this._extra, ...extra }; this._contexts = { ...this._contexts, ...contexts }; @@ -442,6 +534,7 @@ export class Scope { // client is not cleared here on purpose! this._breadcrumbs = []; this._tags = {}; + this._attributes = {}; this._extra = {}; this._user = {}; this._contexts = {}; @@ -528,6 +621,7 @@ export class Scope { attachments: this._attachments, contexts: this._contexts, tags: this._tags, + attributes: this._attributes, extra: this._extra, user: this._user, level: this._level, diff --git a/packages/core/test/lib/attributes.test.ts b/packages/core/test/lib/attributes.test.ts new file mode 100644 index 000000000000..99aa20d07c85 --- /dev/null +++ b/packages/core/test/lib/attributes.test.ts @@ -0,0 +1,286 @@ +import { describe, expect, it } from 'vitest'; +import { attributeValueToTypedAttributeValue, isAttributeObject } from '../../src/attributes'; + +describe('attributeValueToTypedAttributeValue', () => { + describe('primitive values', () => { + it('converts a string value to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue('test'); + expect(result).toStrictEqual({ + value: 'test', + type: 'string', + }); + }); + + it('converts an interger number value to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue(42); + expect(result).toStrictEqual({ + value: 42, + type: 'integer', + }); + }); + + it('converts a double number value to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue(42.34); + expect(result).toStrictEqual({ + value: 42.34, + type: 'double', + }); + }); + + it('converts a boolean value to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue(true); + expect(result).toStrictEqual({ + value: true, + type: 'boolean', + }); + }); + }); + + describe('arrays', () => { + it('converts an array of strings to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue(['foo', 'bar']); + expect(result).toStrictEqual({ + value: ['foo', 'bar'], + type: 'string[]', + }); + }); + + it('converts an array of integer numbers to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue([1, 2, 3]); + expect(result).toStrictEqual({ + value: [1, 2, 3], + type: 'integer[]', + }); + }); + + it('converts an array of double numbers to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue([1.1, 2.2, 3.3]); + expect(result).toStrictEqual({ + value: [1.1, 2.2, 3.3], + type: 'double[]', + }); + }); + + it('converts an array of booleans to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue([true, false, true]); + expect(result).toStrictEqual({ + value: [true, false, true], + type: 'boolean[]', + }); + }); + }); + + describe('attribute objects without units', () => { + // Note: These tests only test exemplar type and fallback behaviour (see above for more cases) + it('converts a primitive value to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue({ value: 123.45 }); + expect(result).toStrictEqual({ + value: 123.45, + type: 'double', + }); + }); + + it('converts an array of primitive values to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue({ value: [true, false] }); + expect(result).toStrictEqual({ + value: [true, false], + type: 'boolean[]', + }); + }); + + it('converts an unsupported object value to a string attribute value', () => { + const result = attributeValueToTypedAttributeValue({ value: { foo: 'bar' } }); + expect(result).toStrictEqual({ + value: '{"foo":"bar"}', + type: 'string', + }); + }); + }); + + describe('attribute objects with units', () => { + // Note: These tests only test exemplar type and fallback behaviour (see above for more cases) + it('converts a primitive value to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue({ value: 123.45, unit: 'ms' }); + expect(result).toStrictEqual({ + value: 123.45, + type: 'double', + unit: 'ms', + }); + }); + + it('converts an array of primitive values to a typed attribute value', () => { + const result = attributeValueToTypedAttributeValue({ value: [true, false], unit: 'count' }); + expect(result).toStrictEqual({ + value: [true, false], + type: 'boolean[]', + unit: 'count', + }); + }); + + it('converts an unsupported object value to a string attribute value', () => { + const result = attributeValueToTypedAttributeValue({ value: { foo: 'bar' }, unit: 'bytes' }); + expect(result).toStrictEqual({ + value: '{"foo":"bar"}', + type: 'string', + unit: 'bytes', + }); + }); + + it('extracts the value property of an object with a value property', () => { + // and ignores other properties. + // For now we're fine with this but we may reconsider in the future. + const result = attributeValueToTypedAttributeValue({ value: 'foo', unit: 'ms', bar: 'baz' }); + expect(result).toStrictEqual({ + value: 'foo', + unit: 'ms', + type: 'string', + }); + }); + }); + + describe('unsupported value types', () => { + it('stringifies mixed float and integer numbers to a string attribute value', () => { + const result = attributeValueToTypedAttributeValue([1, 2.2, 3]); + expect(result).toStrictEqual({ + value: '[1,2.2,3]', + type: 'string', + }); + }); + + it('stringifies an array of allowed but incoherent types to a string attribute value', () => { + const result = attributeValueToTypedAttributeValue([1, 'foo', true]); + expect(result).toStrictEqual({ + value: '[1,"foo",true]', + type: 'string', + }); + }); + + it('stringifies an array of disallowed and incoherent types to a string attribute value', () => { + const result = attributeValueToTypedAttributeValue([null, undefined, NaN]); + expect(result).toStrictEqual({ + value: '[null,null,null]', + type: 'string', + }); + }); + + it('stringifies an object value to a string attribute value', () => { + const result = attributeValueToTypedAttributeValue({ foo: 'bar' }); + expect(result).toStrictEqual({ + value: '{"foo":"bar"}', + type: 'string', + }); + }); + + it('stringifies a null value to a string attribute value', () => { + const result = attributeValueToTypedAttributeValue(null); + expect(result).toStrictEqual({ + value: 'null', + type: 'string', + }); + }); + + it('stringifies an undefined value to a string attribute value', () => { + const result = attributeValueToTypedAttributeValue(undefined); + expect(result).toStrictEqual({ + value: 'undefined', + type: 'string', + }); + }); + + it('stringifies an NaN number value to a string attribute value', () => { + const result = attributeValueToTypedAttributeValue(NaN); + expect(result).toStrictEqual({ + value: 'null', + type: 'string', + }); + }); + + it('converts an object toString if stringification fails', () => { + const result = attributeValueToTypedAttributeValue({ + value: { + toJson: () => { + throw new Error('test'); + }, + }, + }); + expect(result).toStrictEqual({ + value: '{}', + type: 'string', + }); + }); + + it('falls back to an empty string if stringification and toString fails', () => { + const result = attributeValueToTypedAttributeValue({ + value: { + toJSON: () => { + throw new Error('test'); + }, + toString: () => { + throw new Error('test'); + }, + }, + }); + expect(result).toStrictEqual({ + value: '', + type: 'string', + }); + }); + + it('converts a function toString ', () => { + const result = attributeValueToTypedAttributeValue(() => { + return 'test'; + }); + + expect(result).toStrictEqual({ + value: '() => {\n return "test";\n }', + type: 'string', + }); + }); + + it('converts a symbol toString', () => { + const result = attributeValueToTypedAttributeValue(Symbol('test')); + expect(result).toStrictEqual({ + value: 'Symbol(test)', + type: 'string', + }); + }); + }); + + it.each([1, true, null, undefined, NaN, Symbol('test'), { foo: 'bar' }])( + 'ignores invalid (non-string) units (%s)', + unit => { + const result = attributeValueToTypedAttributeValue({ value: 'foo', unit }); + expect(result).toStrictEqual({ + value: 'foo', + type: 'string', + }); + }, + ); +}); + +describe('isAttributeObject', () => { + it.each([ + { value: 123.45, unit: 'ms' }, + { value: [true, false], unit: 'count' }, + { value: { foo: 'bar' }, unit: 'bytes' }, + { value: { value: 123.45, unit: 'ms' }, unit: 'ms' }, + { value: 1 }, + ])('returns true for a valid attribute object (%s)', obj => { + const result = isAttributeObject(obj); + expect(result).toBe(true); + }); + + it('returns true for an object with a value property', () => { + // Explicitly demonstrate this behaviour which for now we're fine with. + // We may reconsider in the future. + expect(isAttributeObject({ value: 123.45, some: 'other property' })).toBe(true); + }); + + it.each([1, true, 'test', null, undefined, NaN, Symbol('test')])( + 'returns false for an invalid attribute object (%s)', + obj => { + const result = isAttributeObject(obj); + expect(result).toBe(false); + }, + ); +}); diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index 221ac14a6fa2..339a57828e5b 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -27,6 +27,7 @@ describe('Scope', () => { attachments: [], contexts: {}, tags: {}, + attributes: {}, extra: {}, user: {}, level: undefined, @@ -42,6 +43,7 @@ describe('Scope', () => { scope.update({ tags: { foo: 'bar' }, extra: { foo2: 'bar2' }, + attributes: { attr1: { value: 'value1' } }, }); expect(scope.getScopeData()).toEqual({ @@ -51,6 +53,7 @@ describe('Scope', () => { tags: { foo: 'bar', }, + attributes: { attr1: { value: 'value1' } }, extra: { foo2: 'bar2', }, @@ -71,6 +74,7 @@ describe('Scope', () => { scope.update({ tags: { foo: 'bar' }, + attributes: { attr1: { value: 'value1', type: 'string' } }, extra: { foo2: 'bar2' }, }); @@ -85,6 +89,7 @@ describe('Scope', () => { tags: { foo: 'bar', }, + attributes: { attr1: { value: 'value1', type: 'string' } }, extra: { foo2: 'bar2', }, @@ -114,7 +119,7 @@ describe('Scope', () => { }); }); - describe('attributes modification', () => { + describe('scope data modification', () => { test('setFingerprint', () => { const scope = new Scope(); scope.setFingerprint(['abcd']); @@ -183,6 +188,159 @@ describe('Scope', () => { }); }); + describe('setAttribute', () => { + it('accepts a key-value pair', () => { + const scope = new Scope(); + + scope.setAttribute('str', 'b'); + scope.setAttribute('int', 1); + scope.setAttribute('double', 1.1); + scope.setAttribute('bool', true); + + expect(scope['_attributes']).toEqual({ + str: 'b', + bool: true, + double: 1.1, + int: 1, + }); + }); + + it('accepts an attribute value object', () => { + const scope = new Scope(); + scope.setAttribute('str', { value: 'b' }); + expect(scope['_attributes']).toEqual({ + str: { value: 'b' }, + }); + }); + + it('accepts an attribute value object with a unit', () => { + const scope = new Scope(); + scope.setAttribute('str', { value: 1, unit: 'millisecond' }); + expect(scope['_attributes']).toEqual({ + str: { value: 1, unit: 'millisecond' }, + }); + }); + + it('still accepts a custom unit but TS-errors on it', () => { + // mostly there for type checking purposes. + const scope = new Scope(); + /** @ts-expect-error we don't support custom units type-wise but we don't actively block them */ + scope.setAttribute('str', { value: 3, unit: 'inch' }); + expect(scope['_attributes']).toEqual({ + str: { value: 3, unit: 'inch' }, + }); + }); + + it('accepts an array', () => { + const scope = new Scope(); + + scope.setAttribute('strArray', ['a', 'b', 'c']); + scope.setAttribute('intArray', { value: [1, 2, 3], unit: 'millisecond' }); + + expect(scope['_attributes']).toEqual({ + strArray: ['a', 'b', 'c'], + intArray: { value: [1, 2, 3], unit: 'millisecond' }, + }); + }); + + it('notifies scope listeners once per call', () => { + const scope = new Scope(); + const listener = vi.fn(); + scope.addScopeListener(listener); + scope.setAttribute('str', 'b'); + scope.setAttribute('int', 1); + expect(listener).toHaveBeenCalledTimes(2); + }); + }); + + describe('setAttributes', () => { + it('accepts key-value pairs', () => { + const scope = new Scope(); + scope.setAttributes({ str: 'b', int: 1, double: 1.1, bool: true }); + expect(scope['_attributes']).toEqual({ + str: 'b', + int: 1, + double: 1.1, + bool: true, + }); + }); + + it('accepts attribute value objects', () => { + const scope = new Scope(); + scope.setAttributes({ str: { value: 'b' }, int: { value: 1 } }); + expect(scope['_attributes']).toEqual({ + str: { value: 'b' }, + int: { value: 1 }, + }); + }); + + it('accepts attribute value objects with units', () => { + const scope = new Scope(); + scope.setAttributes({ str: { value: 'b', unit: 'millisecond' }, int: { value: 12, unit: 'second' } }); + expect(scope['_attributes']).toEqual({ + str: { value: 'b', unit: 'millisecond' }, + int: { value: 12, unit: 'second' }, + }); + }); + + it('accepts arrays', () => { + const scope = new Scope(); + scope.setAttributes({ + strArray: ['a', 'b', 'c'], + intArray: { value: [1, 2, 3], unit: 'millisecond' }, + }); + + expect(scope['_attributes']).toEqual({ + strArray: ['a', 'b', 'c'], + intArray: { value: [1, 2, 3], unit: 'millisecond' }, + }); + }); + + it('notifies scope listeners once per call', () => { + const scope = new Scope(); + const listener = vi.fn(); + scope.addScopeListener(listener); + scope.setAttributes({ str: 'b', int: 1 }); + scope.setAttributes({ bool: true }); + expect(listener).toHaveBeenCalledTimes(2); + }); + }); + + describe('removeAttribute', () => { + it('removes an attribute', () => { + const scope = new Scope(); + scope.setAttribute('str', 'b'); + scope.setAttribute('int', 1); + scope.removeAttribute('str'); + expect(scope['_attributes']).toEqual({ int: 1 }); + }); + + it('notifies scope listeners after deletion', () => { + const scope = new Scope(); + const listener = vi.fn(); + + scope.addScopeListener(listener); + scope.setAttribute('str', { value: 'b' }); + expect(listener).toHaveBeenCalledTimes(1); + + listener.mockClear(); + + scope.removeAttribute('str'); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('does nothing if the attribute does not exist', () => { + const scope = new Scope(); + const listener = vi.fn(); + + scope.addScopeListener(listener); + scope.removeAttribute('str'); + + expect(scope['_attributes']).toEqual({}); + expect(listener).not.toHaveBeenCalled(); + }); + }); + test('setUser', () => { const scope = new Scope(); scope.setUser({ id: '1' }); @@ -329,12 +487,18 @@ describe('Scope', () => { const oldPropagationContext = scope.getScopeData().propagationContext; scope.setExtra('a', 2); scope.setTag('a', 'b'); + scope.setAttribute('c', 'd'); scope.setUser({ id: '1' }); scope.setFingerprint(['abcd']); scope.addBreadcrumb({ message: 'test' }); + + expect(scope['_attributes']).toEqual({ c: 'd' }); expect(scope['_extra']).toEqual({ a: 2 }); + scope.clear(); + expect(scope['_extra']).toEqual({}); + expect(scope['_attributes']).toEqual({}); expect(scope['_propagationContext']).toEqual({ traceId: expect.any(String), sampled: undefined, @@ -357,6 +521,7 @@ describe('Scope', () => { beforeEach(() => { scope = new Scope(); scope.setTags({ foo: '1', bar: '2' }); + scope.setAttribute('attr1', 'value1'); scope.setExtras({ foo: '1', bar: '2' }); scope.setContext('foo', { id: '1' }); scope.setContext('bar', { id: '2' }); From e8a1826167e19bccd5d4bb5bfdb5cbe2f9b70d73 Mon Sep 17 00:00:00 2001 From: Abdul Mateen <59867217+JealousGx@users.noreply.github.com> Date: Sat, 6 Sep 2025 10:44:26 +0500 Subject: [PATCH 20/32] feat(node): Fix local variables capturing for out-of-app frames (#18245) Address an issue where local variables were not being captured for out-of-app frames, even when the `includeOutOfAppFrames` option was enabled. The `localVariablesSyncIntegration` had a race condition where it would process events before the debugger session was fully initialized. Fix this by awaiting the session creation in `setupOnce`. The tests for this integration were failing because they were not setting up a Sentry client, which is required for the integration to be enabled. Correct by adding a client to the test setup. Additionally, add tests for the `localVariablesAsyncIntegration` to ensure it correctly handles the `includeOutOfAppFrames` option. The `LocalVariables` integrations `setupOnce` method was `async`, which violates the `Integration` interface. This caused a race condition where events could be processed before the integration was fully initialized, leading to missed local variables. Fix the race condition by: - Make `setupOnce` synchronous to adhere to the interface contract - Move the asynchronous initialization logic to a separate `setup` function - Make `processEvent` asynchronous and await the result of the `setup` function, so the integration is fully initialized before processing any events - Update tests to correctly `await` the `processEvent` method Fixes GH-12588 Fixes GH-17545 --- .../local-variables-out-of-app-default.js | 28 +++ .../local-variables-out-of-app.js | 33 +++ .../suites/public-api/LocalVariables/test.ts | 73 +++++- .../integrations/local-variables/common.ts | 6 + .../local-variables/local-variables-async.ts | 4 +- .../local-variables/local-variables-sync.ts | 218 +++++++++--------- 6 files changed, 253 insertions(+), 109 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js create mode 100644 dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js new file mode 100644 index 000000000000..9a53436867d9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js @@ -0,0 +1,28 @@ +/* eslint-disable no-unused-vars */ + +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +const externalFunctionFile = require.resolve('./node_modules/out-of-app-function.js'); + +const { out_of_app_function } = require(externalFunctionFile); + +function in_app_function() { + const inAppVar = 'in app value'; + out_of_app_function(`${inAppVar} modified value`); +} + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + includeLocalVariables: true, +}); + +setTimeout(async () => { + try { + in_app_function(); + } catch (e) { + Sentry.captureException(e); + await Sentry.flush(); + } +}, 500); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js new file mode 100644 index 000000000000..9bbe40004fc7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js @@ -0,0 +1,33 @@ +/* eslint-disable no-unused-vars */ + +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +const externalFunctionFile = require.resolve('./node_modules/out-of-app-function.js'); + +const { out_of_app_function } = require(externalFunctionFile); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + includeLocalVariables: true, + integrations: [ + Sentry.localVariablesIntegration({ + includeOutOfAppFrames: true, + }), + ], +}); + +function in_app_function() { + const inAppVar = 'in app value'; + out_of_app_function(`${inAppVar} modified value`); +} + +setTimeout(async () => { + try { + in_app_function(); + } catch (e) { + Sentry.captureException(e); + await Sentry.flush(); + } +}, 500); 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 2c87d14c2b45..6c042d3ecf1f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -1,5 +1,6 @@ +import { mkdirSync, rmdirSync, unlinkSync, writeFileSync } from 'fs'; import * as path from 'path'; -import { afterAll, describe, expect, test } from 'vitest'; +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; import { conditionalTest } from '../../../utils'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; @@ -39,8 +40,35 @@ const EXPECTED_LOCAL_VARIABLES_EVENT = { }; describe('LocalVariables integration', () => { + const nodeModules = `${__dirname}/node_modules`; + const externalModule = `${nodeModules}//out-of-app-function.js`; + function cleanupExternalModuleFile() { + try { + unlinkSync(externalModule); + // eslint-disable-next-line no-empty + } catch {} + try { + rmdirSync(nodeModules); + // eslint-disable-next-line no-empty + } catch {} + } + + beforeAll(() => { + cleanupExternalModuleFile(); + mkdirSync(nodeModules, { recursive: true }); + writeFileSync( + externalModule, + ` +function out_of_app_function(passedArg) { + const outOfAppVar = "out of app value " + passedArg.substring(13); + throw new Error("out-of-app error"); +} +module.exports = { out_of_app_function };`, + ); + }); afterAll(() => { cleanupChildProcesses(); + cleanupExternalModuleFile(); }); test('Should not include local variables by default', async () => { @@ -127,4 +155,47 @@ describe('LocalVariables integration', () => { .start() .completed(); }); + + test('adds local variables to out of app frames when includeOutOfAppFrames is true', async () => { + await createRunner(__dirname, 'local-variables-out-of-app.js') + .expect({ + event: event => { + const frames = event.exception?.values?.[0]?.stacktrace?.frames || []; + + const inAppFrame = frames.find(frame => frame.function === 'in_app_function'); + const outOfAppFrame = frames.find(frame => frame.function === 'out_of_app_function'); + + expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' }); + expect(inAppFrame?.in_app).toEqual(true); + + expect(outOfAppFrame?.vars).toEqual({ + outOfAppVar: 'out of app value modified value', + passedArg: 'in app value modified value', + }); + expect(outOfAppFrame?.in_app).toEqual(false); + }, + }) + .start() + .completed(); + }); + + test('does not add local variables to out of app frames by default', async () => { + await createRunner(__dirname, 'local-variables-out-of-app-default.js') + .expect({ + event: event => { + const frames = event.exception?.values?.[0]?.stacktrace?.frames || []; + + const inAppFrame = frames.find(frame => frame.function === 'in_app_function'); + const outOfAppFrame = frames.find(frame => frame.function === 'out_of_app_function'); + + expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' }); + expect(inAppFrame?.in_app).toEqual(true); + + expect(outOfAppFrame?.vars).toBeUndefined(); + expect(outOfAppFrame?.in_app).toEqual(false); + }, + }) + .start() + .completed(); + }); }); diff --git a/packages/node-core/src/integrations/local-variables/common.ts b/packages/node-core/src/integrations/local-variables/common.ts index 471fa1a69864..f86988b4cbfc 100644 --- a/packages/node-core/src/integrations/local-variables/common.ts +++ b/packages/node-core/src/integrations/local-variables/common.ts @@ -99,6 +99,12 @@ export interface LocalVariablesIntegrationOptions { * Maximum number of exceptions to capture local variables for per second before rate limiting is triggered. */ maxExceptionsPerSecond?: number; + /** + * When true, local variables will be captured for all frames, including those that are not in_app. + * + * Defaults to `false`. + */ + includeOutOfAppFrames?: boolean; } export interface LocalVariablesWorkerArgs extends LocalVariablesIntegrationOptions { diff --git a/packages/node-core/src/integrations/local-variables/local-variables-async.ts b/packages/node-core/src/integrations/local-variables/local-variables-async.ts index 32fff66bab4e..7bad543c2588 100644 --- a/packages/node-core/src/integrations/local-variables/local-variables-async.ts +++ b/packages/node-core/src/integrations/local-variables/local-variables-async.ts @@ -39,8 +39,8 @@ export const localVariablesAsyncIntegration = defineIntegration((( if ( // We need to have vars to add frameLocalVariables.vars === undefined || - // We're not interested in frames that are not in_app because the vars are not relevant - frame.in_app === false || + // Only skip out-of-app frames if includeOutOfAppFrames is not true + (frame.in_app === false && integrationOptions.includeOutOfAppFrames !== true) || // The function names need to match !functionNamesMatch(frame.function, frameLocalVariables.function) ) { diff --git a/packages/node-core/src/integrations/local-variables/local-variables-sync.ts b/packages/node-core/src/integrations/local-variables/local-variables-sync.ts index 7de91a54276e..b2af37b0c7fb 100644 --- a/packages/node-core/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node-core/src/integrations/local-variables/local-variables-sync.ts @@ -268,8 +268,8 @@ const _localVariablesSyncIntegration = (( if ( // We need to have vars to add cachedFrameVariable.vars === undefined || - // We're not interested in frames that are not in_app because the vars are not relevant - frameVariable.in_app === false || + // Only skip out-of-app frames if includeOutOfAppFrames is not true + (frameVariable.in_app === false && options.includeOutOfAppFrames !== true) || // The function names need to match !functionNamesMatch(frameVariable.function, cachedFrameVariable.function) ) { @@ -288,122 +288,128 @@ const _localVariablesSyncIntegration = (( return event; } - return { - name: INTEGRATION_NAME, - async setupOnce() { - const client = getClient(); - const clientOptions = client?.getOptions(); + let setupPromise: Promise | undefined; - if (!clientOptions?.includeLocalVariables) { - return; - } + async function setup(): Promise { + const client = getClient(); + const clientOptions = client?.getOptions(); - // Only setup this integration if the Node version is >= v18 - // https://github.com/getsentry/sentry-javascript/issues/7697 - const unsupportedNodeVersion = NODE_MAJOR < 18; + if (!clientOptions?.includeLocalVariables) { + return; + } - if (unsupportedNodeVersion) { - debug.log('The `LocalVariables` integration is only supported on Node >= v18.'); - return; - } + // Only setup this integration if the Node version is >= v18 + // https://github.com/getsentry/sentry-javascript/issues/7697 + const unsupportedNodeVersion = NODE_MAJOR < 18; - if (await isDebuggerEnabled()) { - debug.warn('Local variables capture has been disabled because the debugger was already enabled'); - return; - } + if (unsupportedNodeVersion) { + debug.log('The `LocalVariables` integration is only supported on Node >= v18.'); + return; + } - AsyncSession.create(sessionOverride).then( - session => { - function handlePaused( - stackParser: StackParser, - { params: { reason, data, callFrames } }: InspectorNotification, - complete: () => void, - ): void { - if (reason !== 'exception' && reason !== 'promiseRejection') { - complete(); - return; - } - - rateLimiter?.(); - - // data.description contains the original error.stack - const exceptionHash = hashFromStack(stackParser, data.description); - - if (exceptionHash == undefined) { - complete(); - return; - } - - const { add, next } = createCallbackList(frames => { - cachedFrames.set(exceptionHash, frames); - complete(); - }); + if (await isDebuggerEnabled()) { + debug.warn('Local variables capture has been disabled because the debugger was already enabled'); + return; + } - // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack - // For this reason we only attempt to get local variables for the first 5 frames - for (let i = 0; i < Math.min(callFrames.length, 5); i++) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { scopeChain, functionName, this: obj } = callFrames[i]!; - - const localScope = scopeChain.find(scope => scope.type === 'local'); - - // obj.className is undefined in ESM modules - const fn = - obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; - - if (localScope?.object.objectId === undefined) { - add(frames => { - frames[i] = { function: fn }; - next(frames); - }); - } else { - const id = localScope.object.objectId; - add(frames => - session.getLocalVariables(id, vars => { - frames[i] = { function: fn, vars }; - next(frames); - }), - ); - } - } - - next([]); - } + try { + const session = await AsyncSession.create(sessionOverride); + + const handlePaused = ( + stackParser: StackParser, + { params: { reason, data, callFrames } }: InspectorNotification, + complete: () => void, + ): void => { + if (reason !== 'exception' && reason !== 'promiseRejection') { + complete(); + return; + } + + rateLimiter?.(); + + // data.description contains the original error.stack + const exceptionHash = hashFromStack(stackParser, data.description); + + if (exceptionHash == undefined) { + complete(); + return; + } - const captureAll = options.captureAllExceptions !== false; - - session.configureAndConnect( - (ev, complete) => - handlePaused(clientOptions.stackParser, ev as InspectorNotification, complete), - captureAll, - ); - - if (captureAll) { - const max = options.maxExceptionsPerSecond || 50; - - rateLimiter = createRateLimiter( - max, - () => { - debug.log('Local variables rate-limit lifted.'); - session.setPauseOnExceptions(true); - }, - seconds => { - debug.log( - `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, - ); - session.setPauseOnExceptions(false); - }, + const { add, next } = createCallbackList(frames => { + cachedFrames.set(exceptionHash, frames); + complete(); + }); + + // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack + // For this reason we only attempt to get local variables for the first 5 frames + for (let i = 0; i < Math.min(callFrames.length, 5); i++) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { scopeChain, functionName, this: obj } = callFrames[i]!; + + const localScope = scopeChain.find(scope => scope.type === 'local'); + + // obj.className is undefined in ESM modules + const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; + + if (localScope?.object.objectId === undefined) { + add(frames => { + frames[i] = { function: fn }; + next(frames); + }); + } else { + const id = localScope.object.objectId; + add(frames => + session.getLocalVariables(id, vars => { + frames[i] = { function: fn, vars }; + next(frames); + }), ); } + } + + next([]); + }; - shouldProcessEvent = true; - }, - error => { - debug.log('The `LocalVariables` integration failed to start.', error); - }, + const captureAll = options.captureAllExceptions !== false; + + session.configureAndConnect( + (ev, complete) => + handlePaused(clientOptions.stackParser, ev as InspectorNotification, complete), + captureAll, ); + + if (captureAll) { + const max = options.maxExceptionsPerSecond || 50; + + rateLimiter = createRateLimiter( + max, + () => { + debug.log('Local variables rate-limit lifted.'); + session.setPauseOnExceptions(true); + }, + seconds => { + debug.log( + `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, + ); + session.setPauseOnExceptions(false); + }, + ); + } + + shouldProcessEvent = true; + } catch (error) { + debug.log('The `LocalVariables` integration failed to start.', error); + } + } + + return { + name: INTEGRATION_NAME, + setupOnce() { + setupPromise = setup(); }, - processEvent(event: Event): Event { + async processEvent(event: Event): Promise { + await setupPromise; + if (shouldProcessEvent) { return addLocalVariablesToEvent(event); } From cbecbdf97b0c402afc324226e63427dfbfc21727 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Sat, 22 Nov 2025 00:11:18 +0100 Subject: [PATCH 21/32] feat(deps): Bump OpenTelemetry instrumentations (#18239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR bumps OpenTelemetry instrumentations and SDK packages to their latest versions. ## Dependency Updates: * @opentelemetry/context-async-hooks: 2.1.0 → 2.2.0 * @opentelemetry/core: 2.1.0 → 2.2.0 * @opentelemetry/resources: 2.1.0 → 2.2.0 * @opentelemetry/sdk-trace-base: 2.1.0 → 2.2.0 * @opentelemetry/sdk-trace-node: 2.1.0 → 2.2.0 * @opentelemetry/instrumentation: 0.204.0 → 0.208.0 * @opentelemetry/instrumentation-mongodb: 0.57.0 → 0.61.0 * @opentelemetry/instrumentation-pg: 0.57.0 → 0.61.0 * @opentelemetry/instrumentation-mysql: 0.50.0 → 0.54.0 * @opentelemetry/instrumentation-mysql2: 0.51.0 → 0.55.0 * @opentelemetry/instrumentation-tedious: 0.23.0 → 0.27.0 * @opentelemetry/instrumentation-mongoose: 0.51.0 → 0.55.0 * @opentelemetry/instrumentation-redis: 0.53.0 → 0.57.0 * @opentelemetry/instrumentation-ioredis: 0.52.0 → 0.56.0 * @opentelemetry/instrumentation-express: 0.53.0 → 0.57.0 * @opentelemetry/instrumentation-koa: 0.52.0 → 0.57.0 * @opentelemetry/instrumentation-hapi: 0.51.0 → 0.55.0 * @opentelemetry/instrumentation-connect: 0.48.0 → 0.52.0 * @opentelemetry/instrumentation-nestjs-core: 0.50.0 → 0.55.0 * @opentelemetry/instrumentation-http: 0.204.0 → 0.208.0 * @opentelemetry/instrumentation-graphql: 0.52.0 → 0.56.0 * @opentelemetry/instrumentation-amqplib: 0.51.0 → 0.55.0 * @opentelemetry/instrumentation-aws-sdk: 0.59.0 → 0.64.0 * @opentelemetry/instrumentation-dataloader: 0.22.0 → 0.26.0 * @opentelemetry/instrumentation-fs: 0.24.0 → 0.28.0 * @opentelemetry/instrumentation-generic-pool: 0.48.0 → 0.52.0 * @opentelemetry/instrumentation-kafkajs: 0.14.0 → 0.18.0 * @opentelemetry/instrumentation-knex: 0.49.0 → 0.53.0 * @opentelemetry/instrumentation-lru-memoizer: 0.49.0 → 0.53.0 * @opentelemetry/instrumentation-undici: 0.15.0 → 0.19.0 * @prisma/instrumentation: 6.15.0 → 6.19.0 Closes: #18178 --- CHANGELOG.md | 33 ++ .../test-applications/nextjs-16/package.json | 4 +- .../package.json | 12 +- .../package.json | 16 +- .../node-core-express-otel-v2/package.json | 12 +- .../node-otel-sdk-node/package.json | 4 +- .../node-otel-without-tracing/package.json | 8 +- .../tests/transactions.test.ts | 4 + .../test-applications/node-otel/package.json | 4 +- .../node-core-integration-tests/package.json | 12 +- packages/aws-serverless/package.json | 4 +- packages/nestjs/package.json | 6 +- packages/node-core/package.json | 20 +- .../src/integrations/diagnostic_channel.d.ts | 556 ------------------ packages/node/package.json | 58 +- packages/opentelemetry/package.json | 12 +- packages/react-router/package.json | 4 +- packages/remix/package.json | 2 +- packages/sveltekit/src/client/sdk.ts | 4 +- packages/vercel-edge/package.json | 6 +- .../abstract-async-hooks-context-manager.ts | 14 +- .../async-local-storage-context-manager.ts | 12 +- yarn.lock | 423 ++++++------- 23 files changed, 347 insertions(+), 883 deletions(-) delete mode 100644 packages/node-core/src/integrations/diagnostic_channel.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eba860932aa..479b72fc2f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +- feat(deps): Bump OpenTelemetry ([#18239](https://github.com/getsentry/sentry-javascript/pull/18239)) + - Bump @opentelemetry/context-async-hooks from ^2.1.0 to ^2.2.0 + - Bump @opentelemetry/core from ^2.1.0 to ^2.2.0 + - Bump @opentelemetry/resources from ^2.1.0 to ^2.2.0 + - Bump @opentelemetry/sdk-trace-base from ^2.1.0 to ^2.2.0 + - Bump @opentelemetry/sdk-trace-node from ^2.1.0 to ^2.2.0 + - Bump @opentelemetry/instrumentation from 0.204.0 to 0.208.0 + - Bump @opentelemetry/instrumentation-amqplib from 0.51.0 to 0.55.0 + - Bump @opentelemetry/instrumentation-aws-sdk from 0.59.0 to 0.64.0 + - Bump @opentelemetry/instrumentation-connect from 0.48.0 to 0.52.0 + - Bump @opentelemetry/instrumentation-dataloader from 0.22.0 to 0.26.0 + - Bump @opentelemetry/instrumentation-express from 0.53.0 to 0.57.0 + - Bump @opentelemetry/instrumentation-fs from 0.24.0 to 0.28.0 + - Bump @opentelemetry/instrumentation-generic-pool from 0.48.0 to 0.52.0 + - Bump @opentelemetry/instrumentation-graphql from 0.52.0 to 0.56.0 + - Bump @opentelemetry/instrumentation-hapi from 0.51.0 to 0.55.0 + - Bump @opentelemetry/instrumentation-http from 0.204.0 to 0.208.0 + - Bump @opentelemetry/instrumentation-ioredis from 0.52.0 to 0.56.0 + - Bump @opentelemetry/instrumentation-kafkajs from 0.14.0 to 0.18.0 + - Bump @opentelemetry/instrumentation-knex from 0.49.0 to 0.53.0 + - Bump @opentelemetry/instrumentation-koa from 0.52.0 to 0.57.0 + - Bump @opentelemetry/instrumentation-lru-memoizer from 0.49.0 to 0.53.0 + - Bump @opentelemetry/instrumentation-mongodb from 0.57.0 to 0.61.0 + - Bump @opentelemetry/instrumentation-mongoose from 0.51.0 to 0.55.0 + - Bump @opentelemetry/instrumentation-mysql from 0.50.0 to 0.54.0 + - Bump @opentelemetry/instrumentation-mysql2 from 0.51.0 to 0.55.0 + - Bump @opentelemetry/instrumentation-nestjs-core from 0.50.0 to 0.55.0 + - Bump @opentelemetry/instrumentation-pg from 0.57.0 to 0.61.0 + - Bump @opentelemetry/instrumentation-redis from 0.53.0 to 0.57.0 + - Bump @opentelemetry/instrumentation-tedious from 0.23.0 to 0.27.0 + - Bump @opentelemetry/instrumentation-undici from 0.15.0 to 0.19.0 + - Bump @prisma/instrumentation from 6.15.0 to 6.19.0 + ## 10.26.0 ### Important Changes diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json index af9f306f017d..662e1b85936a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json @@ -26,11 +26,11 @@ "@sentry/nextjs": "latest || *", "@sentry/core": "latest || *", "ai": "^3.0.0", - "import-in-the-middle": "^1", + "import-in-the-middle": "^2", "next": "16.0.0", "react": "19.1.0", "react-dom": "19.1.0", - "require-in-the-middle": "^7", + "require-in-the-middle": "^8", "zod": "^3.22.4" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json index e29a40c2887e..5710105d4ab8 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json @@ -12,12 +12,12 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.1.0", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/instrumentation-http": "^0.204.0", - "@opentelemetry/resources": "^2.1.0", - "@opentelemetry/sdk-trace-node": "^2.1.0", + "@opentelemetry/context-async-hooks": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-http": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-trace-node": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@sentry/node-core": "latest || *", "@sentry/opentelemetry": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json index 34b050f350c1..f6074d159bbe 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json @@ -12,15 +12,15 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.1.0", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/instrumentation-http": "^0.204.0", - "@opentelemetry/resources": "^2.1.0", - "@opentelemetry/sdk-trace-node": "^2.1.0", + "@opentelemetry/context-async-hooks": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-http": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-trace-node": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@opentelemetry/sdk-node": "^0.204.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.204.0", + "@opentelemetry/sdk-node": "^0.208.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", "@sentry/node-core": "latest || *", "@sentry/opentelemetry": "latest || *", "@types/express": "4.17.17", diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json index 2252750e423e..b9ba557d67b5 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json @@ -14,12 +14,12 @@ "@sentry/node-core": "latest || *", "@sentry/opentelemetry": "latest || *", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.1.0", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/instrumentation-http": "^0.204.0", - "@opentelemetry/resources": "^2.1.0", - "@opentelemetry/sdk-trace-node": "^2.1.0", + "@opentelemetry/context-async-hooks": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-http": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-trace-node": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@types/express": "^4.17.21", "@types/node": "^18.19.1", diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json index 7296f72218cd..1eb93f281cf8 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json @@ -11,8 +11,8 @@ "test:assert": "pnpm test" }, "dependencies": { - "@opentelemetry/sdk-node": "0.204.0", - "@opentelemetry/exporter-trace-otlp-http": "0.204.0", + "@opentelemetry/sdk-node": "0.208.0", + "@opentelemetry/exporter-trace-otlp-http": "0.208.0", "@sentry/node": "latest || *", "@types/express": "4.17.17", "@types/node": "^18.19.1", diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json index f13daab2ef6c..fc153ddceeb8 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json @@ -11,11 +11,11 @@ "test:assert": "pnpm test" }, "dependencies": { - "@opentelemetry/sdk-trace-node": "2.1.0", - "@opentelemetry/exporter-trace-otlp-http": "0.204.0", + "@opentelemetry/sdk-trace-node": "2.2.0", + "@opentelemetry/exporter-trace-otlp-http": "0.208.0", "@opentelemetry/instrumentation-undici": "0.13.2", - "@opentelemetry/instrumentation-http": "0.204.0", - "@opentelemetry/instrumentation": "0.204.0", + "@opentelemetry/instrumentation-http": "0.208.0", + "@opentelemetry/instrumentation": "0.208.0", "@sentry/node": "latest || *", "@types/express": "4.17.17", "@types/node": "^18.19.1", diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts index 678841bdb249..26c9d7de5496 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts @@ -65,6 +65,7 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => { status: { code: 0 }, links: [], droppedLinksCount: 0, + flags: expect.any(Number), }, { traceId: expect.any(String), @@ -80,6 +81,7 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => { status: { code: 0 }, links: [], droppedLinksCount: 0, + flags: expect.any(Number), }, ]); @@ -116,6 +118,7 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => { status: { code: 0 }, links: [], droppedLinksCount: 0, + flags: expect.any(Number), }, ]); @@ -157,6 +160,7 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => { }, links: [], droppedLinksCount: 0, + flags: expect.any(Number), }, ]); }); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/package.json b/dev-packages/e2e-tests/test-applications/node-otel/package.json index 31cf99c32c91..e2b7086f23ba 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel/package.json +++ b/dev-packages/e2e-tests/test-applications/node-otel/package.json @@ -11,8 +11,8 @@ "test:assert": "pnpm test" }, "dependencies": { - "@opentelemetry/sdk-node": "0.204.0", - "@opentelemetry/exporter-trace-otlp-http": "0.204.0", + "@opentelemetry/sdk-node": "0.208.0", + "@opentelemetry/exporter-trace-otlp-http": "0.208.0", "@sentry/node": "latest || *", "@types/express": "4.17.17", "@types/node": "^18.19.1", diff --git a/dev-packages/node-core-integration-tests/package.json b/dev-packages/node-core-integration-tests/package.json index fe755f16cc6d..24ac2f57ea9e 100644 --- a/dev-packages/node-core-integration-tests/package.json +++ b/dev-packages/node-core-integration-tests/package.json @@ -27,12 +27,12 @@ "@nestjs/core": "^11", "@nestjs/platform-express": "^11", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.1.0", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/instrumentation-http": "0.204.0", - "@opentelemetry/resources": "^2.1.0", - "@opentelemetry/sdk-trace-base": "^2.1.0", + "@opentelemetry/context-async-hooks": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-http": "0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@sentry/core": "10.26.0", "@sentry/node-core": "10.26.0", diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index cd0ad16d9e7c..8d6360e82d2a 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -66,8 +66,8 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/instrumentation-aws-sdk": "0.59.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-aws-sdk": "0.64.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@sentry/core": "10.26.0", "@sentry/node": "10.26.0", diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 53c3064ed08f..ae39e2dc5d4f 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -45,9 +45,9 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/instrumentation-nestjs-core": "0.50.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-nestjs-core": "0.55.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@sentry/core": "10.26.0", "@sentry/node": "10.26.0" diff --git a/packages/node-core/package.json b/packages/node-core/package.json index 89dbe5461165..1f845acfa16b 100644 --- a/packages/node-core/package.json +++ b/packages/node-core/package.json @@ -58,27 +58,27 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", - "@opentelemetry/resources": "^1.30.1 || ^2.1.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", + "@opentelemetry/resources": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0" }, "dependencies": { "@apm-js-collab/tracing-hooks": "^0.3.1", "@sentry/core": "10.26.0", "@sentry/opentelemetry": "10.26.0", - "import-in-the-middle": "^1.14.2" + "import-in-the-middle": "^2" }, "devDependencies": { "@apm-js-collab/code-transformer": "^0.8.2", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.1.0", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/resources": "^2.1.0", - "@opentelemetry/sdk-trace-base": "^2.1.0", + "@opentelemetry/context-async-hooks": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@types/node": "^18.19.1" }, diff --git a/packages/node-core/src/integrations/diagnostic_channel.d.ts b/packages/node-core/src/integrations/diagnostic_channel.d.ts deleted file mode 100644 index abf3649a617f..000000000000 --- a/packages/node-core/src/integrations/diagnostic_channel.d.ts +++ /dev/null @@ -1,556 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/ban-types */ -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ - -/** - * The `node:diagnostics_channel` module provides an API to create named channels - * to report arbitrary message data for diagnostics purposes. - * - * It can be accessed using: - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * ``` - * - * It is intended that a module writer wanting to report diagnostics messages - * will create one or many top-level channels to report messages through. - * Channels may also be acquired at runtime but it is not encouraged - * due to the additional overhead of doing so. Channels may be exported for - * convenience, but as long as the name is known it can be acquired anywhere. - * - * If you intend for your module to produce diagnostics data for others to - * consume it is recommended that you include documentation of what named - * channels are used along with the shape of the message data. Channel names - * should generally include the module name to avoid collisions with data from - * other modules. - * @since v15.1.0, v14.17.0 - * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/diagnostics_channel.js) - */ -declare module 'diagnostics_channel' { - import type { AsyncLocalStorage } from 'node:async_hooks'; - /** - * Check if there are active subscribers to the named channel. This is helpful if - * the message you want to send might be expensive to prepare. - * - * This API is optional but helpful when trying to publish messages from very - * performance-sensitive code. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * if (diagnostics_channel.hasSubscribers('my-channel')) { - * // There are subscribers, prepare and publish message - * } - * ``` - * @since v15.1.0, v14.17.0 - * @param name The channel name - * @return If there are active subscribers - */ - function hasSubscribers(name: string | symbol): boolean; - /** - * This is the primary entry-point for anyone wanting to publish to a named - * channel. It produces a channel object which is optimized to reduce overhead at - * publish time as much as possible. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * ``` - * @since v15.1.0, v14.17.0 - * @param name The channel name - * @return The named channel object - */ - function channel(name: string | symbol): Channel; - type ChannelListener = (message: unknown, name: string | symbol) => void; - /** - * Register a message handler to subscribe to this channel. This message handler - * will be run synchronously whenever a message is published to the channel. Any - * errors thrown in the message handler will trigger an `'uncaughtException'`. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * diagnostics_channel.subscribe('my-channel', (message, name) => { - * // Received data - * }); - * ``` - * @since v18.7.0, v16.17.0 - * @param name The channel name - * @param onMessage The handler to receive channel messages - */ - function subscribe(name: string | symbol, onMessage: ChannelListener): void; - /** - * Remove a message handler previously registered to this channel with {@link subscribe}. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * function onMessage(message, name) { - * // Received data - * } - * - * diagnostics_channel.subscribe('my-channel', onMessage); - * - * diagnostics_channel.unsubscribe('my-channel', onMessage); - * ``` - * @since v18.7.0, v16.17.0 - * @param name The channel name - * @param onMessage The previous subscribed handler to remove - * @return `true` if the handler was found, `false` otherwise. - */ - function unsubscribe(name: string | symbol, onMessage: ChannelListener): boolean; - /** - * Creates a `TracingChannel` wrapper for the given `TracingChannel Channels`. If a name is given, the corresponding tracing - * channels will be created in the form of `tracing:${name}:${eventType}` where `eventType` corresponds to the types of `TracingChannel Channels`. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channelsByName = diagnostics_channel.tracingChannel('my-channel'); - * - * // or... - * - * const channelsByCollection = diagnostics_channel.tracingChannel({ - * start: diagnostics_channel.channel('tracing:my-channel:start'), - * end: diagnostics_channel.channel('tracing:my-channel:end'), - * asyncStart: diagnostics_channel.channel('tracing:my-channel:asyncStart'), - * asyncEnd: diagnostics_channel.channel('tracing:my-channel:asyncEnd'), - * error: diagnostics_channel.channel('tracing:my-channel:error'), - * }); - * ``` - * @since v19.9.0 - * @experimental - * @param nameOrChannels Channel name or object containing all the `TracingChannel Channels` - * @return Collection of channels to trace with - */ - function tracingChannel< - StoreType = unknown, - ContextType extends object = StoreType extends object ? StoreType : object, - >(nameOrChannels: string | TracingChannelCollection): TracingChannel; - /** - * The class `Channel` represents an individual named channel within the data - * pipeline. It is used to track subscribers and to publish messages when there - * are subscribers present. It exists as a separate object to avoid channel - * lookups at publish time, enabling very fast publish speeds and allowing - * for heavy use while incurring very minimal cost. Channels are created with {@link channel}, constructing a channel directly - * with `new Channel(name)` is not supported. - * @since v15.1.0, v14.17.0 - */ - class Channel { - readonly name: string | symbol; - /** - * Check if there are active subscribers to this channel. This is helpful if - * the message you want to send might be expensive to prepare. - * - * This API is optional but helpful when trying to publish messages from very - * performance-sensitive code. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * if (channel.hasSubscribers) { - * // There are subscribers, prepare and publish message - * } - * ``` - * @since v15.1.0, v14.17.0 - */ - readonly hasSubscribers: boolean; - private constructor(name: string | symbol); - /** - * Publish a message to any subscribers to the channel. This will trigger - * message handlers synchronously so they will execute within the same context. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * channel.publish({ - * some: 'message', - * }); - * ``` - * @since v15.1.0, v14.17.0 - * @param message The message to send to the channel subscribers - */ - publish(message: unknown): void; - /** - * Register a message handler to subscribe to this channel. This message handler - * will be run synchronously whenever a message is published to the channel. Any - * errors thrown in the message handler will trigger an `'uncaughtException'`. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * channel.subscribe((message, name) => { - * // Received data - * }); - * ``` - * @since v15.1.0, v14.17.0 - * @deprecated Since v18.7.0,v16.17.0 - Use {@link subscribe(name, onMessage)} - * @param onMessage The handler to receive channel messages - */ - subscribe(onMessage: ChannelListener): void; - /** - * Remove a message handler previously registered to this channel with `channel.subscribe(onMessage)`. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * function onMessage(message, name) { - * // Received data - * } - * - * channel.subscribe(onMessage); - * - * channel.unsubscribe(onMessage); - * ``` - * @since v15.1.0, v14.17.0 - * @deprecated Since v18.7.0,v16.17.0 - Use {@link unsubscribe(name, onMessage)} - * @param onMessage The previous subscribed handler to remove - * @return `true` if the handler was found, `false` otherwise. - */ - unsubscribe(onMessage: ChannelListener): void; - /** - * When `channel.runStores(context, ...)` is called, the given context data - * will be applied to any store bound to the channel. If the store has already been - * bound the previous `transform` function will be replaced with the new one. - * The `transform` function may be omitted to set the given context data as the - * context directly. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * import { AsyncLocalStorage } from 'node:async_hooks'; - * - * const store = new AsyncLocalStorage(); - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * channel.bindStore(store, (data) => { - * return { data }; - * }); - * ``` - * @since v19.9.0 - * @experimental - * @param store The store to which to bind the context data - * @param transform Transform context data before setting the store context - */ - bindStore(store: AsyncLocalStorage, transform?: (context: ContextType) => StoreType): void; - /** - * Remove a message handler previously registered to this channel with `channel.bindStore(store)`. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * import { AsyncLocalStorage } from 'node:async_hooks'; - * - * const store = new AsyncLocalStorage(); - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * channel.bindStore(store); - * channel.unbindStore(store); - * ``` - * @since v19.9.0 - * @experimental - * @param store The store to unbind from the channel. - * @return `true` if the store was found, `false` otherwise. - */ - unbindStore(store: any): void; - /** - * Applies the given data to any AsyncLocalStorage instances bound to the channel - * for the duration of the given function, then publishes to the channel within - * the scope of that data is applied to the stores. - * - * If a transform function was given to `channel.bindStore(store)` it will be - * applied to transform the message data before it becomes the context value for - * the store. The prior storage context is accessible from within the transform - * function in cases where context linking is required. - * - * The context applied to the store should be accessible in any async code which - * continues from execution which began during the given function, however - * there are some situations in which `context loss` may occur. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * import { AsyncLocalStorage } from 'node:async_hooks'; - * - * const store = new AsyncLocalStorage(); - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * channel.bindStore(store, (message) => { - * const parent = store.getStore(); - * return new Span(message, parent); - * }); - * channel.runStores({ some: 'message' }, () => { - * store.getStore(); // Span({ some: 'message' }) - * }); - * ``` - * @since v19.9.0 - * @experimental - * @param context Message to send to subscribers and bind to stores - * @param fn Handler to run within the entered storage context - * @param thisArg The receiver to be used for the function call. - * @param args Optional arguments to pass to the function. - */ - runStores(): void; - } - interface TracingChannelSubscribers { - start: (message: ContextType) => void; - end: ( - message: ContextType & { - error?: unknown; - result?: unknown; - }, - ) => void; - asyncStart: ( - message: ContextType & { - error?: unknown; - result?: unknown; - }, - ) => void; - asyncEnd: ( - message: ContextType & { - error?: unknown; - result?: unknown; - }, - ) => void; - error: ( - message: ContextType & { - error: unknown; - }, - ) => void; - } - interface TracingChannelCollection { - start: Channel; - end: Channel; - asyncStart: Channel; - asyncEnd: Channel; - error: Channel; - } - /** - * The class `TracingChannel` is a collection of `TracingChannel Channels` which - * together express a single traceable action. It is used to formalize and - * simplify the process of producing events for tracing application flow. {@link tracingChannel} is used to construct a `TracingChannel`. As with `Channel` it is recommended to create and reuse a - * single `TracingChannel` at the top-level of the file rather than creating them - * dynamically. - * @since v19.9.0 - * @experimental - */ - class TracingChannel implements TracingChannelCollection { - start: Channel; - end: Channel; - asyncStart: Channel; - asyncEnd: Channel; - error: Channel; - /** - * Helper to subscribe a collection of functions to the corresponding channels. - * This is the same as calling `channel.subscribe(onMessage)` on each channel - * individually. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channels = diagnostics_channel.tracingChannel('my-channel'); - * - * channels.subscribe({ - * start(message) { - * // Handle start message - * }, - * end(message) { - * // Handle end message - * }, - * asyncStart(message) { - * // Handle asyncStart message - * }, - * asyncEnd(message) { - * // Handle asyncEnd message - * }, - * error(message) { - * // Handle error message - * }, - * }); - * ``` - * @since v19.9.0 - * @experimental - * @param subscribers Set of `TracingChannel Channels` subscribers - */ - subscribe(subscribers: TracingChannelSubscribers): void; - /** - * Helper to unsubscribe a collection of functions from the corresponding channels. - * This is the same as calling `channel.unsubscribe(onMessage)` on each channel - * individually. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channels = diagnostics_channel.tracingChannel('my-channel'); - * - * channels.unsubscribe({ - * start(message) { - * // Handle start message - * }, - * end(message) { - * // Handle end message - * }, - * asyncStart(message) { - * // Handle asyncStart message - * }, - * asyncEnd(message) { - * // Handle asyncEnd message - * }, - * error(message) { - * // Handle error message - * }, - * }); - * ``` - * @since v19.9.0 - * @experimental - * @param subscribers Set of `TracingChannel Channels` subscribers - * @return `true` if all handlers were successfully unsubscribed, and `false` otherwise. - */ - unsubscribe(subscribers: TracingChannelSubscribers): void; - /** - * Trace a synchronous function call. This will always produce a `start event` and `end event` around the execution and may produce an `error event` if the given function throws an error. - * This will run the given function using `channel.runStores(context, ...)` on the `start` channel which ensures all - * events should have any bound stores set to match this trace context. - * - * To ensure only correct trace graphs are formed, events will only be published if subscribers are present prior to starting the trace. Subscriptions - * which are added after the trace begins will not receive future events from that trace, only future traces will be seen. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channels = diagnostics_channel.tracingChannel('my-channel'); - * - * channels.traceSync(() => { - * // Do something - * }, { - * some: 'thing', - * }); - * ``` - * @since v19.9.0 - * @experimental - * @param fn Function to wrap a trace around - * @param context Shared object to correlate events through - * @param thisArg The receiver to be used for the function call - * @param args Optional arguments to pass to the function - * @return The return value of the given function - */ - traceSync( - fn: (this: ThisArg, ...args: Args) => any, - context?: ContextType, - thisArg?: ThisArg, - ...args: Args - ): void; - /** - * Trace a promise-returning function call. This will always produce a `start event` and `end event` around the synchronous portion of the - * function execution, and will produce an `asyncStart event` and `asyncEnd event` when a promise continuation is reached. It may also - * produce an `error event` if the given function throws an error or the - * returned promise rejects. This will run the given function using `channel.runStores(context, ...)` on the `start` channel which ensures all - * events should have any bound stores set to match this trace context. - * - * To ensure only correct trace graphs are formed, events will only be published if subscribers are present prior to starting the trace. Subscriptions - * which are added after the trace begins will not receive future events from that trace, only future traces will be seen. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channels = diagnostics_channel.tracingChannel('my-channel'); - * - * channels.tracePromise(async () => { - * // Do something - * }, { - * some: 'thing', - * }); - * ``` - * @since v19.9.0 - * @experimental - * @param fn Promise-returning function to wrap a trace around - * @param context Shared object to correlate trace events through - * @param thisArg The receiver to be used for the function call - * @param args Optional arguments to pass to the function - * @return Chained from promise returned by the given function - */ - tracePromise( - fn: (this: ThisArg, ...args: Args) => Promise, - context?: ContextType, - thisArg?: ThisArg, - ...args: Args - ): void; - /** - * Trace a callback-receiving function call. This will always produce a `start event` and `end event` around the synchronous portion of the - * function execution, and will produce a `asyncStart event` and `asyncEnd event` around the callback execution. It may also produce an `error event` if the given function throws an error or - * the returned - * promise rejects. This will run the given function using `channel.runStores(context, ...)` on the `start` channel which ensures all - * events should have any bound stores set to match this trace context. - * - * The `position` will be -1 by default to indicate the final argument should - * be used as the callback. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * - * const channels = diagnostics_channel.tracingChannel('my-channel'); - * - * channels.traceCallback((arg1, callback) => { - * // Do something - * callback(null, 'result'); - * }, 1, { - * some: 'thing', - * }, thisArg, arg1, callback); - * ``` - * - * The callback will also be run with `channel.runStores(context, ...)` which - * enables context loss recovery in some cases. - * - * To ensure only correct trace graphs are formed, events will only be published if subscribers are present prior to starting the trace. Subscriptions - * which are added after the trace begins will not receive future events from that trace, only future traces will be seen. - * - * ```js - * import diagnostics_channel from 'node:diagnostics_channel'; - * import { AsyncLocalStorage } from 'node:async_hooks'; - * - * const channels = diagnostics_channel.tracingChannel('my-channel'); - * const myStore = new AsyncLocalStorage(); - * - * // The start channel sets the initial store data to something - * // and stores that store data value on the trace context object - * channels.start.bindStore(myStore, (data) => { - * const span = new Span(data); - * data.span = span; - * return span; - * }); - * - * // Then asyncStart can restore from that data it stored previously - * channels.asyncStart.bindStore(myStore, (data) => { - * return data.span; - * }); - * ``` - * @since v19.9.0 - * @experimental - * @param fn callback using function to wrap a trace around - * @param position Zero-indexed argument position of expected callback - * @param context Shared object to correlate trace events through - * @param thisArg The receiver to be used for the function call - * @param args Optional arguments to pass to the function - * @return The return value of the given function - */ - traceCallback any>( - fn: Fn, - position?: number, - context?: ContextType, - thisArg?: any, - ...args: Parameters - ): void; - } -} -declare module 'node:diagnostics_channel' { - export * from 'diagnostics_channel'; -} diff --git a/packages/node/package.json b/packages/node/package.json index 6f0bec49c92a..e43d7b04a0ee 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -66,39 +66,39 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.1.0", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/instrumentation-amqplib": "0.51.0", - "@opentelemetry/instrumentation-connect": "0.48.0", - "@opentelemetry/instrumentation-dataloader": "0.22.0", - "@opentelemetry/instrumentation-express": "0.53.0", - "@opentelemetry/instrumentation-fs": "0.24.0", - "@opentelemetry/instrumentation-generic-pool": "0.48.0", - "@opentelemetry/instrumentation-graphql": "0.52.0", - "@opentelemetry/instrumentation-hapi": "0.51.0", - "@opentelemetry/instrumentation-http": "0.204.0", - "@opentelemetry/instrumentation-ioredis": "0.52.0", - "@opentelemetry/instrumentation-kafkajs": "0.14.0", - "@opentelemetry/instrumentation-knex": "0.49.0", - "@opentelemetry/instrumentation-koa": "0.52.0", - "@opentelemetry/instrumentation-lru-memoizer": "0.49.0", - "@opentelemetry/instrumentation-mongodb": "0.57.0", - "@opentelemetry/instrumentation-mongoose": "0.51.0", - "@opentelemetry/instrumentation-mysql": "0.50.0", - "@opentelemetry/instrumentation-mysql2": "0.51.0", - "@opentelemetry/instrumentation-pg": "0.57.0", - "@opentelemetry/instrumentation-redis": "0.53.0", - "@opentelemetry/instrumentation-tedious": "0.23.0", - "@opentelemetry/instrumentation-undici": "0.15.0", - "@opentelemetry/resources": "^2.1.0", - "@opentelemetry/sdk-trace-base": "^2.1.0", + "@opentelemetry/context-async-hooks": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-amqplib": "0.55.0", + "@opentelemetry/instrumentation-connect": "0.52.0", + "@opentelemetry/instrumentation-dataloader": "0.26.0", + "@opentelemetry/instrumentation-express": "0.57.0", + "@opentelemetry/instrumentation-fs": "0.28.0", + "@opentelemetry/instrumentation-generic-pool": "0.52.0", + "@opentelemetry/instrumentation-graphql": "0.56.0", + "@opentelemetry/instrumentation-hapi": "0.55.0", + "@opentelemetry/instrumentation-http": "0.208.0", + "@opentelemetry/instrumentation-ioredis": "0.56.0", + "@opentelemetry/instrumentation-kafkajs": "0.18.0", + "@opentelemetry/instrumentation-knex": "0.53.0", + "@opentelemetry/instrumentation-koa": "0.57.0", + "@opentelemetry/instrumentation-lru-memoizer": "0.53.0", + "@opentelemetry/instrumentation-mongodb": "0.61.0", + "@opentelemetry/instrumentation-mongoose": "0.55.0", + "@opentelemetry/instrumentation-mysql": "0.54.0", + "@opentelemetry/instrumentation-mysql2": "0.55.0", + "@opentelemetry/instrumentation-pg": "0.61.0", + "@opentelemetry/instrumentation-redis": "0.57.0", + "@opentelemetry/instrumentation-tedious": "0.27.0", + "@opentelemetry/instrumentation-undici": "0.19.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@prisma/instrumentation": "6.15.0", + "@prisma/instrumentation": "6.19.0", "@sentry/core": "10.26.0", "@sentry/node-core": "10.26.0", "@sentry/opentelemetry": "10.26.0", - "import-in-the-middle": "^1.14.2", + "import-in-the-middle": "^2", "minimatch": "^9.0.0" }, "devDependencies": { diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 95b7f54cd000..86d73b8555b2 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -43,16 +43,16 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0" }, "devDependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.1.0", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/sdk-trace-base": "^2.1.0", + "@opentelemetry/context-async-hooks": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0" }, "scripts": { diff --git a/packages/react-router/package.json b/packages/react-router/package.json index a65bd845bdab..51ce7bb94122 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -46,8 +46,8 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/instrumentation": "^0.208.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@sentry/browser": "10.26.0", "@sentry/cli": "^2.58.2", diff --git a/packages/remix/package.json b/packages/remix/package.json index 181d6e23a63f..98bdd9a39c7c 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -65,7 +65,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/instrumentation": "^0.204.0", + "@opentelemetry/instrumentation": "^0.208.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@remix-run/router": "1.x", "@sentry/cli": "^2.58.2", diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts index 5c3f482cb7d0..a6294ad25977 100644 --- a/packages/sveltekit/src/client/sdk.ts +++ b/packages/sveltekit/src/client/sdk.ts @@ -64,7 +64,7 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] | undefi * @returns the function that was previously on `window.fetch`. */ function switchToFetchProxy(): typeof fetch | undefined { - const globalWithSentryFetchProxy: WindowWithSentryFetchProxy = WINDOW as WindowWithSentryFetchProxy; + const globalWithSentryFetchProxy: WindowWithSentryFetchProxy = WINDOW; // eslint-disable-next-line @typescript-eslint/unbound-method const actualFetch = globalWithSentryFetchProxy.fetch; @@ -81,7 +81,7 @@ function switchToFetchProxy(): typeof fetch | undefined { * and puts our fetch proxy back onto `window._sentryFetchProxy`. */ function restoreFetch(actualFetch: typeof fetch): void { - const globalWithSentryFetchProxy: WindowWithSentryFetchProxy = WINDOW as WindowWithSentryFetchProxy; + const globalWithSentryFetchProxy: WindowWithSentryFetchProxy = WINDOW; // eslint-disable-next-line @typescript-eslint/unbound-method globalWithSentryFetchProxy._sentryFetchProxy = globalWithSentryFetchProxy.fetch; diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index d5c9ef78b8c1..1efd9c1af34c 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -40,13 +40,13 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/resources": "^2.1.0", + "@opentelemetry/resources": "^2.2.0", "@sentry/core": "10.26.0" }, "devDependencies": { "@edge-runtime/types": "3.0.1", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/sdk-trace-base": "^2.1.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@sentry/opentelemetry": "10.26.0" }, diff --git a/packages/vercel-edge/src/vendored/abstract-async-hooks-context-manager.ts b/packages/vercel-edge/src/vendored/abstract-async-hooks-context-manager.ts index 72373afdebdf..3ec1934af3f7 100644 --- a/packages/vercel-edge/src/vendored/abstract-async-hooks-context-manager.ts +++ b/packages/vercel-edge/src/vendored/abstract-async-hooks-context-manager.ts @@ -32,10 +32,22 @@ /* eslint-disable @typescript-eslint/no-this-alias */ import type { Context, ContextManager } from '@opentelemetry/api'; -import type { EventEmitter } from 'events'; type Func = (...args: unknown[]) => T; +// Inline EventEmitter interface to avoid Node.js module dependency +// This prevents Node.js type leaks in edge runtime environments +interface EventEmitter { + addListener?(event: string, listener: Func): this; + on?(event: string, listener: Func): this; + once?(event: string, listener: Func): this; + prependListener?(event: string, listener: Func): this; + prependOnceListener?(event: string, listener: Func): this; + removeListener?(event: string, listener: Func): this; + off?(event: string, listener: Func): this; + removeAllListeners?(event?: string): this; +} + /** * Store a map for each event of all original listeners and their "patched" * version. So when a listener is removed by the user, the corresponding diff --git a/packages/vercel-edge/src/vendored/async-local-storage-context-manager.ts b/packages/vercel-edge/src/vendored/async-local-storage-context-manager.ts index 3fd89f28af7c..257c6c27f041 100644 --- a/packages/vercel-edge/src/vendored/async-local-storage-context-manager.ts +++ b/packages/vercel-edge/src/vendored/async-local-storage-context-manager.ts @@ -28,10 +28,17 @@ import type { Context } from '@opentelemetry/api'; import { ROOT_CONTEXT } from '@opentelemetry/api'; import { debug, GLOBAL_OBJ } from '@sentry/core'; -import type { AsyncLocalStorage } from 'async_hooks'; import { DEBUG_BUILD } from '../debug-build'; import { AbstractAsyncHooksContextManager } from './abstract-async-hooks-context-manager'; +// Inline AsyncLocalStorage interface to avoid Node.js module dependency +// This prevents Node.js type leaks in edge runtime environments +interface AsyncLocalStorage { + getStore(): T | undefined; + run(store: T, callback: (...args: any[]) => R, ...args: any[]): R; + disable(): void; +} + export class AsyncLocalStorageContextManager extends AbstractAsyncHooksContextManager { private _asyncLocalStorage: AsyncLocalStorage; @@ -46,12 +53,11 @@ export class AsyncLocalStorageContextManager extends AbstractAsyncHooksContextMa "Tried to register AsyncLocalStorage async context strategy in a runtime that doesn't support AsyncLocalStorage.", ); - // @ts-expect-error Vendored type shenanigans this._asyncLocalStorage = { getStore() { return undefined; }, - run(_store: unknown, callback: () => Context, ...args: unknown[]) { + run(_store: Context, callback: (...args: any[]) => R, ...args: any[]): R { return callback.apply(this, args); }, disable() { diff --git a/yarn.lock b/yarn.lock index 8687df6cfa53..41c2c2fe1486 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5959,17 +5959,10 @@ dependencies: "@octokit/openapi-types" "^18.0.0" -"@opentelemetry/api-logs@0.204.0": - version "0.204.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.204.0.tgz#c0285aa5c79625a1c424854393902d21732fd76b" - integrity sha512-DqxY8yoAaiBPivoJD4UtgrMS8gEmzZ5lnaxzPojzLVHBGqPxgWm4zcuvcUHZiqQ6kRX2Klel2r9y8cA2HAtqpw== - dependencies: - "@opentelemetry/api" "^1.3.0" - -"@opentelemetry/api-logs@0.57.2": - version "0.57.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz#d4001b9aa3580367b40fe889f3540014f766cc87" - integrity sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A== +"@opentelemetry/api-logs@0.208.0": + version "0.208.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz#56d3891010a1fa1cf600ba8899ed61b43ace511c" + integrity sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg== dependencies: "@opentelemetry/api" "^1.3.0" @@ -5978,277 +5971,260 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== -"@opentelemetry/context-async-hooks@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-2.1.0.tgz#de1de21d9536abfe73769f822b52a59a8c97b083" - integrity sha512-zOyetmZppnwTyPrt4S7jMfXiSX9yyfF0hxlA8B5oo2TtKl+/RGCy7fi4DrBfIf3lCPrkKsRBWZZD7RFojK7FDg== +"@opentelemetry/context-async-hooks@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz#5465f6fad6350f52cf9d95a92907a3a464d50644" + integrity sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ== -"@opentelemetry/core@2.1.0", "@opentelemetry/core@^2.0.0", "@opentelemetry/core@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.1.0.tgz#5539f04eb9e5245e000b0c3f77bdfaa07557e3a7" - integrity sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ== +"@opentelemetry/core@2.2.0", "@opentelemetry/core@^2.0.0", "@opentelemetry/core@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.2.0.tgz#2f857d7790ff160a97db3820889b5f4cade6eaee" + integrity sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw== dependencies: "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/instrumentation-amqplib@0.51.0": - version "0.51.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.51.0.tgz#1779326433f1ab8a743bbf8e1957e1b1252cf036" - integrity sha512-XGmjYwjVRktD4agFnWBWQXo9SiYHKBxR6Ag3MLXwtLE4R99N3a08kGKM5SC1qOFKIELcQDGFEFT9ydXMH00Luw== +"@opentelemetry/instrumentation-amqplib@0.55.0": + version "0.55.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.55.0.tgz#4d1afc47e7690693efa690ed06fbda3acc585a2f" + integrity sha512-5ULoU8p+tWcQw5PDYZn8rySptGSLZHNX/7srqo2TioPnAAcvTy6sQFQXsNPrAnyRRtYGMetXVyZUy5OaX1+IfA== dependencies: "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.204.0" - "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/instrumentation" "^0.208.0" -"@opentelemetry/instrumentation-aws-sdk@0.59.0": - version "0.59.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.59.0.tgz#bd612836a6158f1773369a5646984b95f805273d" - integrity sha512-GN/9YGBMb//s0vnchM2jMCkCaIKDB/Piau72fcuqcDXNBffMgu+AA9vCHZD2umriciXLtXJ2GXTh2/yaaHwLIw== +"@opentelemetry/instrumentation-aws-sdk@0.64.0": + version "0.64.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.64.0.tgz#714508bde88be99c936f2191c7ba7f54ccdb5bc0" + integrity sha512-8+Y8IcUfME5jD03LISBcd9sFipgOon2uAoiLKSCroiGD6MPuwMzqlVvhlKSzq7uxwtZIhR6CTmjCpLsCHum59A== dependencies: "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" "@opentelemetry/semantic-conventions" "^1.34.0" -"@opentelemetry/instrumentation-connect@0.48.0": - version "0.48.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.48.0.tgz#4481c84315b33b54a67c6e787be0eb72a84b23b3" - integrity sha512-OMjc3SFL4pC16PeK+tDhwP7MRvDPalYCGSvGqUhX5rASkI2H0RuxZHOWElYeXkV0WP+70Gw6JHWac/2Zqwmhdw== +"@opentelemetry/instrumentation-connect@0.52.0": + version "0.52.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.52.0.tgz#60cde91c548e9da4528ae47fe69af41d05eeb485" + integrity sha512-GXPxfNB5szMbV3I9b7kNWSmQBoBzw7MT0ui6iU/p+NIzVx3a06Ri2cdQO7tG9EKb4aKSLmfX9Cw5cKxXqX6Ohg== dependencies: "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@types/connect" "3.4.38" -"@opentelemetry/instrumentation-dataloader@0.22.0": - version "0.22.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.22.0.tgz#a34f8ac6ec18e8f1585dcd89f9f611570868d1a2" - integrity sha512-bXnTcwtngQsI1CvodFkTemrrRSQjAjZxqHVc+CJZTDnidT0T6wt3jkKhnsjU/Kkkc0lacr6VdRpCu2CUWa0OKw== +"@opentelemetry/instrumentation-dataloader@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.26.0.tgz#d10d22854ee8eac4471c82b8862b177a40f3bf8e" + integrity sha512-P2BgnFfTOarZ5OKPmYfbXfDFjQ4P9WkQ1Jji7yH5/WwB6Wm/knynAoA1rxbjWcDlYupFkyT0M1j6XLzDzy0aCA== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" -"@opentelemetry/instrumentation-express@0.53.0": - version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.53.0.tgz#902634e3de640bd4fa5370924397e716608ecb90" - integrity sha512-r/PBafQmFYRjuxLYEHJ3ze1iBnP2GDA1nXOSS6E02KnYNZAVjj6WcDA1MSthtdAUUK0XnotHvvWM8/qz7DMO5A== +"@opentelemetry/instrumentation-express@0.57.0": + version "0.57.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.57.0.tgz#7a2a7e90a84ad6c109f42c15acabdc7f6646a412" + integrity sha512-HAdx/o58+8tSR5iW+ru4PHnEejyKrAy9fYFhlEI81o10nYxrGahnMAHWiSjhDC7UQSY3I4gjcPgSKQz4rm/asg== dependencies: "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-fs@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.24.0.tgz#edf0f7418f6a1cdcbe135857ab75629e7d94b910" - integrity sha512-HjIxJ6CBRD770KNVaTdMXIv29Sjz4C1kPCCK5x1Ujpc6SNnLGPqUVyJYZ3LUhhnHAqdbrl83ogVWjCgeT4Q0yw== +"@opentelemetry/instrumentation-fs@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.28.0.tgz#6387fb7c19213afa31a2eb1b646d6356b95176bf" + integrity sha512-FFvg8fq53RRXVBRHZViP+EMxMR03tqzEGpuq55lHNbVPyFklSVfQBN50syPhK5UYYwaStx0eyCtHtbRreusc5g== dependencies: "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" -"@opentelemetry/instrumentation-generic-pool@0.48.0": - version "0.48.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.48.0.tgz#76fc08d76515db04f3833d730c5cb18cb0b237d4" - integrity sha512-TLv/On8pufynNR+pUbpkyvuESVASZZKMlqCm4bBImTpXKTpqXaJJ3o/MUDeMlM91rpen+PEv2SeyOKcHCSlgag== +"@opentelemetry/instrumentation-generic-pool@0.52.0": + version "0.52.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.52.0.tgz#12b57774ca3664edb9649687674320955e025906" + integrity sha512-ISkNcv5CM2IwvsMVL31Tl61/p2Zm2I2NAsYq5SSBgOsOndT0TjnptjufYVScCnD5ZLD1tpl4T3GEYULLYOdIdQ== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" -"@opentelemetry/instrumentation-graphql@0.52.0": - version "0.52.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.52.0.tgz#a2d23a669bdd0a1b031f785fe447d5a34ac56343" - integrity sha512-3fEJ8jOOMwopvldY16KuzHbRhPk8wSsOTSF0v2psmOCGewh6ad+ZbkTx/xyUK9rUdUMWAxRVU0tFpj4Wx1vkPA== +"@opentelemetry/instrumentation-graphql@0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.56.0.tgz#77464dec65efe5aa53d8787d0760534cf2e2a88f" + integrity sha512-IPvNk8AFoVzTAM0Z399t34VDmGDgwT6rIqCUug8P9oAGerl2/PEIYMPOl/rerPGu+q8gSWdmbFSjgg7PDVRd3Q== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" -"@opentelemetry/instrumentation-hapi@0.51.0": - version "0.51.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.51.0.tgz#879926dfbb2e1609cc8658392167b1456c75d9e0" - integrity sha512-qyf27DaFNL1Qhbo/da+04MSCw982B02FhuOS5/UF+PMhM61CcOiu7fPuXj8TvbqyReQuJFljXE6UirlvoT/62g== +"@opentelemetry/instrumentation-hapi@0.55.0": + version "0.55.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.55.0.tgz#a687b9bddfcc484f2cc85f022c123f83c19883a4" + integrity sha512-prqAkRf9e4eEpy4G3UcR32prKE8NLNlA90TdEU1UsghOTg0jUvs40Jz8LQWFEs5NbLbXHYGzB4CYVkCI8eWEVQ== dependencies: "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-http@0.204.0": - version "0.204.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.204.0.tgz#faaf009b75e6a68729923b0a2a5270dc7d336f1d" - integrity sha512-1afJYyGRA4OmHTv0FfNTrTAzoEjPQUYgd+8ih/lX0LlZBnGio/O80vxA0lN3knsJPS7FiDrsDrWq25K7oAzbkw== +"@opentelemetry/instrumentation-http@0.208.0": + version "0.208.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.208.0.tgz#64fcc02bfbc80eb3bbb91cd3c7e0e24c695f2bef" + integrity sha512-rhmK46DRWEbQQB77RxmVXGyjs6783crXCnFjYQj+4tDH/Kpv9Rbg3h2kaNyp5Vz2emF1f9HOQQvZoHzwMWOFZQ== dependencies: - "@opentelemetry/core" "2.1.0" - "@opentelemetry/instrumentation" "0.204.0" + "@opentelemetry/core" "2.2.0" + "@opentelemetry/instrumentation" "0.208.0" "@opentelemetry/semantic-conventions" "^1.29.0" forwarded-parse "2.1.2" -"@opentelemetry/instrumentation-ioredis@0.52.0": - version "0.52.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.52.0.tgz#ca5d7b1a49798ed2d29a0f212a7ca5ef95c173c5" - integrity sha512-rUvlyZwI90HRQPYicxpDGhT8setMrlHKokCtBtZgYxQWRF5RBbG4q0pGtbZvd7kyseuHbFpA3I/5z7M8b/5ywg== +"@opentelemetry/instrumentation-ioredis@0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.56.0.tgz#9b89cca6c3e440ae9e896f81dc6d2ab1dfee2581" + integrity sha512-XSWeqsd3rKSsT3WBz/JKJDcZD4QYElZEa0xVdX8f9dh4h4QgXhKRLorVsVkK3uXFbC2sZKAS2Ds+YolGwD83Dg== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" - "@opentelemetry/redis-common" "^0.38.0" - "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/instrumentation" "^0.208.0" + "@opentelemetry/redis-common" "^0.38.2" -"@opentelemetry/instrumentation-kafkajs@0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.14.0.tgz#ffc30728b5845907d2c5b9f3883676c754ef4927" - integrity sha512-kbB5yXS47dTIdO/lfbbXlzhvHFturbux4EpP0+6H78Lk0Bn4QXiZQW7rmZY1xBCY16mNcCb8Yt0mhz85hTnSVA== +"@opentelemetry/instrumentation-kafkajs@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.18.0.tgz#b836e6883afb7ca6df9fd3b6e024408dcc5e584b" + integrity sha512-KCL/1HnZN5zkUMgPyOxfGjLjbXjpd4odDToy+7c+UsthIzVLFf99LnfIBE8YSSrYE4+uS7OwJMhvhg3tWjqMBg== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" "@opentelemetry/semantic-conventions" "^1.30.0" -"@opentelemetry/instrumentation-knex@0.49.0": - version "0.49.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.49.0.tgz#8c04c80c00ead5fbdf600cd2460dcd21b4069157" - integrity sha512-NKsRRT27fbIYL4Ix+BjjP8h4YveyKc+2gD6DMZbr5R5rUeDqfC8+DTfIt3c3ex3BIc5Vvek4rqHnN7q34ZetLQ== +"@opentelemetry/instrumentation-knex@0.53.0": + version "0.53.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.53.0.tgz#c2158c9259ff6789f6c2849bfd3c319edc0fcdf6" + integrity sha512-xngn5cH2mVXFmiT1XfQ1aHqq1m4xb5wvU6j9lSgLlihJ1bXzsO543cpDwjrZm2nMrlpddBf55w8+bfS4qDh60g== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" "@opentelemetry/semantic-conventions" "^1.33.1" -"@opentelemetry/instrumentation-koa@0.52.0": - version "0.52.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.52.0.tgz#7266785ea85334366c3e50dc2b45468df438eb3f" - integrity sha512-JJSBYLDx/mNSy8Ibi/uQixu2rH0bZODJa8/cz04hEhRaiZQoeJ5UrOhO/mS87IdgVsHrnBOsZ6vDu09znupyuA== +"@opentelemetry/instrumentation-koa@0.57.0": + version "0.57.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.57.0.tgz#9a9edcde7de472f7f03904c00d31d87c6ee0ee42" + integrity sha512-3JS8PU/D5E3q295mwloU2v7c7/m+DyCqdu62BIzWt+3u9utjxC9QS7v6WmUNuoDN3RM+Q+D1Gpj13ERo+m7CGg== dependencies: "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.204.0" - "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/instrumentation" "^0.208.0" + "@opentelemetry/semantic-conventions" "^1.36.0" -"@opentelemetry/instrumentation-lru-memoizer@0.49.0": - version "0.49.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.49.0.tgz#6353b877628339e3f07189f4fb15919a73fe1503" - integrity sha512-ctXu+O/1HSadAxtjoEg2w307Z5iPyLOMM8IRNwjaKrIpNAthYGSOanChbk1kqY6zU5CrpkPHGdAT6jk8dXiMqw== +"@opentelemetry/instrumentation-lru-memoizer@0.53.0": + version "0.53.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.53.0.tgz#936c05263b719ee66999a9240b82fded044ebd2c" + integrity sha512-LDwWz5cPkWWr0HBIuZUjslyvijljTwmwiItpMTHujaULZCxcYE9eU44Qf/pbVC8TulT0IhZi+RoGvHKXvNhysw== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" -"@opentelemetry/instrumentation-mongodb@0.57.0": - version "0.57.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.57.0.tgz#e697261b2eac05280134e1851b72c89d5b4b3da8" - integrity sha512-KD6Rg0KSHWDkik+qjIOWoksi1xqSpix8TSPfquIK1DTmd9OTFb5PHmMkzJe16TAPVEuElUW8gvgP59cacFcrMQ== +"@opentelemetry/instrumentation-mongodb@0.61.0": + version "0.61.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.61.0.tgz#4db130d537d630c3089115d2d214d29bcfb49f41" + integrity sha512-OV3i2DSoY5M/pmLk+68xr5RvkHU8DRB3DKMzYJdwDdcxeLs62tLbkmRyqJZsYf3Ht7j11rq35pHOWLuLzXL7pQ== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" - "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/instrumentation" "^0.208.0" -"@opentelemetry/instrumentation-mongoose@0.51.0": - version "0.51.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.51.0.tgz#688e9f3448e3d0979c4aaab5b566e14f30a1aa72" - integrity sha512-gwWaAlhhV2By7XcbyU3DOLMvzsgeaymwP/jktDC+/uPkCmgB61zurwqOQdeiRq9KAf22Y2dtE5ZLXxytJRbEVA== +"@opentelemetry/instrumentation-mongoose@0.55.0": + version "0.55.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.55.0.tgz#e6851aba996b23b9709143c2b640084e92313dea" + integrity sha512-5afj0HfF6aM6Nlqgu6/PPHFk8QBfIe3+zF9FGpX76jWPS0/dujoEYn82/XcLSaW5LPUDW8sni+YeK0vTBNri+w== dependencies: "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.204.0" - "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/instrumentation" "^0.208.0" -"@opentelemetry/instrumentation-mysql2@0.51.0": - version "0.51.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.51.0.tgz#7eec3a0b9e4b27759df5df1c82eaedcf34b27528" - integrity sha512-zT2Wg22Xn43RyfU3NOUmnFtb5zlDI0fKcijCj9AcK9zuLZ4ModgtLXOyBJSSfO+hsOCZSC1v/Fxwj+nZJFdzLQ== +"@opentelemetry/instrumentation-mysql2@0.55.0": + version "0.55.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.55.0.tgz#a0957590aa8d402d1debd10e42d7b5da359164ec" + integrity sha512-0cs8whQG55aIi20gnK8B7cco6OK6N+enNhW0p5284MvqJ5EPi+I1YlWsWXgzv/V2HFirEejkvKiI4Iw21OqDWg== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" - "@opentelemetry/semantic-conventions" "^1.27.0" - "@opentelemetry/sql-common" "^0.41.0" + "@opentelemetry/instrumentation" "^0.208.0" + "@opentelemetry/semantic-conventions" "^1.33.0" + "@opentelemetry/sql-common" "^0.41.2" -"@opentelemetry/instrumentation-mysql@0.50.0": - version "0.50.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.50.0.tgz#25de9de05191cecf8b01df379544eba50fa6f548" - integrity sha512-duKAvMRI3vq6u9JwzIipY9zHfikN20bX05sL7GjDeLKr2qV0LQ4ADtKST7KStdGcQ+MTN5wghWbbVdLgNcB3rA== +"@opentelemetry/instrumentation-mysql@0.54.0": + version "0.54.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.54.0.tgz#6181ae097a2b5501049c518fe90393e1f136341d" + integrity sha512-bqC1YhnwAeWmRzy1/Xf9cDqxNG2d/JDkaxnqF5N6iJKN1eVWI+vg7NfDkf52/Nggp3tl1jcC++ptC61BD6738A== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" - "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/instrumentation" "^0.208.0" "@types/mysql" "2.15.27" -"@opentelemetry/instrumentation-nestjs-core@0.50.0": - version "0.50.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.50.0.tgz#f803bbeb6c972ac8f0685885cca7f6e5a4e09056" - integrity sha512-10u2Gjw260W8vdUem6pM7ENrb8i+UAyrgouhjN7HRdQYh9rcit51tRhgrI52fxTsRjrrBNIItHkX0YM8WnEU2w== +"@opentelemetry/instrumentation-nestjs-core@0.55.0": + version "0.55.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.55.0.tgz#820391be7ed2b699b49fef55b78619832ac0e0ae" + integrity sha512-JFLNhbbEGnnQrMKOYoXx0nNk5N9cPeghu4xP/oup40a7VaSeYruyOiFbg9nkbS4ZQiI8aMuRqUT3Mo4lQjKEKg== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" "@opentelemetry/semantic-conventions" "^1.30.0" -"@opentelemetry/instrumentation-pg@0.57.0": - version "0.57.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.57.0.tgz#346cb613ccd1100221cef9692271468a3fe92eb0" - integrity sha512-dWLGE+r5lBgm2A8SaaSYDE3OKJ/kwwy5WLyGyzor8PLhUL9VnJRiY6qhp4njwhnljiLtzeffRtG2Mf/YyWLeTw== +"@opentelemetry/instrumentation-pg@0.61.0": + version "0.61.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.61.0.tgz#c755d00dba640e229fe50f817423dcf3376957ab" + integrity sha512-UeV7KeTnRSM7ECHa3YscoklhUtTQPs6V6qYpG283AB7xpnPGCUCUfECFT9jFg6/iZOQTt3FHkB1wGTJCNZEvPw== dependencies: "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.204.0" + "@opentelemetry/instrumentation" "^0.208.0" "@opentelemetry/semantic-conventions" "^1.34.0" - "@opentelemetry/sql-common" "^0.41.0" - "@types/pg" "8.15.5" + "@opentelemetry/sql-common" "^0.41.2" + "@types/pg" "8.15.6" "@types/pg-pool" "2.0.6" -"@opentelemetry/instrumentation-redis@0.53.0": - version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.53.0.tgz#826cfeacebaf7ce571bb932ad410f23caf170b9c" - integrity sha512-WUHV8fr+8yo5RmzyU7D5BIE1zwiaNQcTyZPwtxlfr7px6NYYx7IIpSihJK7WA60npWynfxxK1T67RAVF0Gdfjg== +"@opentelemetry/instrumentation-redis@0.57.0": + version "0.57.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.57.0.tgz#c6996eb8ace9cb16cf5be3db3a6b0fb599f47fab" + integrity sha512-bCxTHQFXzrU3eU1LZnOZQ3s5LURxQPDlU3/upBzlWY77qOI1GZuGofazj3jtzjctMJeBEJhNwIFEgRPBX1kp/Q== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" - "@opentelemetry/redis-common" "^0.38.0" + "@opentelemetry/instrumentation" "^0.208.0" + "@opentelemetry/redis-common" "^0.38.2" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-tedious@0.23.0": - version "0.23.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.23.0.tgz#a781de2cb33ff71ef65bbefba11c9fe2d79c4b32" - integrity sha512-3TMTk/9VtlRonVTaU4tCzbg4YqW+Iq/l5VnN2e5whP6JgEg/PKfrGbqQ+CxQWNLfLaQYIUgEZqAn5gk/inh1uQ== +"@opentelemetry/instrumentation-tedious@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.27.0.tgz#f4ba662fd17edde80f1b14d0dc4c42c7fa4a3139" + integrity sha512-jRtyUJNZppPBjPae4ZjIQ2eqJbcRaRfJkr0lQLHFmOU/no5A6e9s1OHLd5XZyZoBJ/ymngZitanyRRA5cniseA== dependencies: - "@opentelemetry/instrumentation" "^0.204.0" - "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/instrumentation" "^0.208.0" "@types/tedious" "^4.0.14" -"@opentelemetry/instrumentation-undici@0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.15.0.tgz#c8193a162d4abe61c2fd247912e0cb8c0c3bc10c" - integrity sha512-sNFGA/iCDlVkNjzTzPRcudmI11vT/WAfAguRdZY9IspCw02N4WSC72zTuQhSMheh2a1gdeM9my1imnKRvEEvEg== +"@opentelemetry/instrumentation-undici@0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.19.0.tgz#a9db59a7630261269239d17d2990d406e2ecddf8" + integrity sha512-Pst/RhR61A2OoZQZkn6OLpdVpXp6qn3Y92wXa6umfJe9rV640r4bc6SWvw4pPN6DiQqPu2c8gnSSZPDtC6JlpQ== dependencies: "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.204.0" - -"@opentelemetry/instrumentation@0.204.0", "@opentelemetry/instrumentation@^0.204.0": - version "0.204.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.204.0.tgz#587c104c02c9ccb38932ce508d9c81514ec7a7c4" - integrity sha512-vV5+WSxktzoMP8JoYWKeopChy6G3HKk4UQ2hESCRDUUTZqQ3+nM3u8noVG0LmNfRWwcFBnbZ71GKC7vaYYdJ1g== - dependencies: - "@opentelemetry/api-logs" "0.204.0" - import-in-the-middle "^1.8.1" - require-in-the-middle "^7.1.1" - -"@opentelemetry/instrumentation@^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0": - version "0.57.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz#8924549d7941ba1b5c6f04d5529cf48330456d1d" - integrity sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg== - dependencies: - "@opentelemetry/api-logs" "0.57.2" - "@types/shimmer" "^1.2.0" - import-in-the-middle "^1.8.1" - require-in-the-middle "^7.1.1" - semver "^7.5.2" - shimmer "^1.2.1" - -"@opentelemetry/redis-common@^0.38.0": - version "0.38.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/redis-common/-/redis-common-0.38.0.tgz#87d2a792dcbcf466a41bb7dfb8a7cd094d643d0b" - integrity sha512-4Wc0AWURII2cfXVVoZ6vDqK+s5n4K5IssdrlVrvGsx6OEOKdghKtJZqXAHWFiZv4nTDLH2/2fldjIHY8clMOjQ== - -"@opentelemetry/resources@2.1.0", "@opentelemetry/resources@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.1.0.tgz#11772e732af4f27953cf55567a6630d8b4d8282d" - integrity sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw== + "@opentelemetry/instrumentation" "^0.208.0" + "@opentelemetry/semantic-conventions" "^1.24.0" + +"@opentelemetry/instrumentation@0.208.0", "@opentelemetry/instrumentation@>=0.52.0 <1", "@opentelemetry/instrumentation@^0.208.0": + version "0.208.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz#d764f8e4329dad50804e2e98f010170c14c4ce8f" + integrity sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA== + dependencies: + "@opentelemetry/api-logs" "0.208.0" + import-in-the-middle "^2.0.0" + require-in-the-middle "^8.0.0" + +"@opentelemetry/redis-common@^0.38.2": + version "0.38.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz#cefa4f3e79db1cd54f19e233b7dfb56621143955" + integrity sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA== + +"@opentelemetry/resources@2.2.0", "@opentelemetry/resources@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.2.0.tgz#b90a950ad98551295b76ea8a0e7efe45a179badf" + integrity sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A== dependencies: - "@opentelemetry/core" "2.1.0" + "@opentelemetry/core" "2.2.0" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/sdk-trace-base@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.1.0.tgz#9d31474824e9ed215f94bf71260d5321f64d402a" - integrity sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ== +"@opentelemetry/sdk-trace-base@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz#ddef9a0afd01a623d8625a3529f2137b05e67d0b" + integrity sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw== dependencies: - "@opentelemetry/core" "2.1.0" - "@opentelemetry/resources" "2.1.0" + "@opentelemetry/core" "2.2.0" + "@opentelemetry/resources" "2.2.0" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.1", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.37.0": - version "1.37.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz#aa2b4fa0b910b66a050c5ddfcac1d262e91a321a" - integrity sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA== +"@opentelemetry/semantic-conventions@^1.24.0", "@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.0", "@opentelemetry/semantic-conventions@^1.33.1", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.36.0", "@opentelemetry/semantic-conventions@^1.37.0": + version "1.38.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz#8b5f415395a7ddb7c8e0c7932171deb9278df1a3" + integrity sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg== -"@opentelemetry/sql-common@^0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sql-common/-/sql-common-0.41.0.tgz#7ddef1ea7fb6338dcca8a9d2485c7dfd53c076b4" - integrity sha512-pmzXctVbEERbqSfiAgdes9Y63xjoOyXcD7B6IXBkVb+vbM7M9U98mn33nGXxPf4dfYR0M+vhcKRZmbSJ7HfqFA== +"@opentelemetry/sql-common@^0.41.2": + version "0.41.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz#7f4a14166cfd6c9ffe89096db1cc75eaf6443b19" + integrity sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ== dependencies: "@opentelemetry/core" "^2.0.0" @@ -6456,12 +6432,12 @@ dependencies: "@prisma/debug" "6.15.0" -"@prisma/instrumentation@6.15.0": - version "6.15.0" - resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.15.0.tgz#40b066dc6b1ea621aa5ae0fd6d54319550b7d8c9" - integrity sha512-6TXaH6OmDkMOQvOxwLZ8XS51hU2v4A3vmE2pSijCIiGRJYyNeMcL6nMHQMyYdZRD8wl7LF3Wzc+AMPMV/9Oo7A== +"@prisma/instrumentation@6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.19.0.tgz#46d15adc8bc4a5a3167032eea6d0a7aa64fb7d93" + integrity sha512-QcuYy25pkXM8BJ37wVFBO7Zh34nyRV1GOb2n3lPkkbRYfl4hWl3PTcImP41P0KrzVXfa/45p6eVCos27x3exIg== dependencies: - "@opentelemetry/instrumentation" "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" + "@opentelemetry/instrumentation" ">=0.52.0 <1" "@protobuf-ts/plugin-framework@^2.0.7", "@protobuf-ts/plugin-framework@^2.9.4": version "2.9.4" @@ -8947,10 +8923,10 @@ dependencies: "@types/pg" "*" -"@types/pg@*", "@types/pg@8.15.5", "@types/pg@^8.6.5": - version "8.15.5" - resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.15.5.tgz#ef43e0f33b62dac95cae2f042888ec7980b30c09" - integrity sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ== +"@types/pg@*", "@types/pg@8.15.6", "@types/pg@^8.6.5": + version "8.15.6" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.15.6.tgz#4df7590b9ac557cbe5479e0074ec1540cbddad9b" + integrity sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ== dependencies: "@types/node" "*" pg-protocol "*" @@ -9097,11 +9073,6 @@ "@types/mime" "*" "@types/node" "*" -"@types/shimmer@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.2.0.tgz#9b706af96fa06416828842397a70dfbbf1c14ded" - integrity sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg== - "@types/sinon@^17.0.3": version "17.0.3" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.3.tgz#9aa7e62f0a323b9ead177ed23a36ea757141a5fa" @@ -19113,10 +19084,10 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@^1.14.2, import-in-the-middle@^1.8.1: - version "1.14.2" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz#283661625a88ff7c0462bd2984f77715c3bc967c" - integrity sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw== +import-in-the-middle@^2, import-in-the-middle@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-2.0.0.tgz#295948cee94d0565314824c6bd75379d13e5b1a5" + integrity sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A== dependencies: acorn "^8.14.0" acorn-import-attributes "^1.9.5" @@ -26741,14 +26712,13 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -require-in-the-middle@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz#b539de8f00955444dc8aed95e17c69b0a4f10fcf" - integrity sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw== +require-in-the-middle@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz#dbde2587f669398626d56b20c868ab87bf01cce4" + integrity sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ== dependencies: - debug "^4.1.1" + debug "^4.3.5" module-details-from-path "^1.0.3" - resolve "^1.22.1" require-package-name@^2.0.1: version "2.0.1" @@ -27487,7 +27457,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semve resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.0, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.6.3, semver@^7.7.2: +semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.0, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.6.3, semver@^7.7.2: version "7.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== @@ -27773,11 +27743,6 @@ shikiji@^0.9.12: dependencies: shikiji-core "0.9.19" -shimmer@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" - integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== - side-channel-list@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" From 5e7cd0687fc242201f7f1dbf36de5ae686e9ca54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Bignon?= Date: Mon, 24 Nov 2025 10:02:04 +0100 Subject: [PATCH 22/32] feat(node): Add tracing support for AzureOpenAI (#18281) This pull request adds the support to Azure OpenAI client in addition to the existing support of the vanilla OpenAI client. Fixes issue #18280 --- .../tracing/openai/scenario-azure-openai.mjs | 64 +++++++++++++++++++ .../suites/tracing/openai/test.ts | 47 ++++++++++++++ .../openai/v6/scenario-azure-openai.mjs | 64 +++++++++++++++++++ .../suites/tracing/openai/v6/test.ts | 58 +++++++++++++++++ .../tracing/openai/instrumentation.ts | 22 +++++-- 5 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/openai/scenario-azure-openai.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/openai/v6/scenario-azure-openai.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/scenario-azure-openai.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/scenario-azure-openai.mjs new file mode 100644 index 000000000000..6d519ae8b313 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/openai/scenario-azure-openai.mjs @@ -0,0 +1,64 @@ +import express from 'express'; +import { AzureOpenAI } from 'openai'; + +function startMockOpenAiServer() { + const app = express(); + app.use(express.json()); + + app.post('/azureopenai/deployments/:model/chat/completions', (req, res) => { + res.send({ + id: 'chatcmpl-mock123', + object: 'chat.completion', + created: 1677652288, + model: req.body.model, + system_fingerprint: 'fp_44709d6fcb', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'Hello from OpenAI mock!', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 15, + total_tokens: 25, + }, + }); + }); + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const server = await startMockOpenAiServer(); + + const client = new AzureOpenAI({ + baseURL: `http://localhost:${server.address().port}/azureopenai`, + apiKey: 'mock-api-key', + apiVersion: '2024-02-15-preview', + }); + + const response = await client.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [ + { role: 'system', content: 'You are a helpful assistant.' }, + { role: 'user', content: 'What is the capital of France?' }, + ], + temperature: 0.7, + max_tokens: 100, + }); + + // eslint-disable-next-line no-console + console.log(JSON.stringify(response)); + + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts index 116c3a6208fa..a0436d9e5a8b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -501,6 +501,53 @@ describe('OpenAI integration', () => { }); }); + createEsmAndCjsTests(__dirname, 'scenario-azure-openai.mjs', 'instrument.mjs', (createRunner, test) => { + test('it works with Azure OpenAI', async () => { + await createRunner() + // First the span that our mock express server is emitting, unrelated to this test + .expect({ + transaction: { + transaction: 'POST /azureopenai/deployments/:model/chat/completions', + }, + }) + .expect({ + transaction: { + transaction: 'chat gpt-3.5-turbo', + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'gen_ai.operation.name': 'chat', + 'sentry.op': 'gen_ai.chat', + 'sentry.origin': 'auto.ai.openai', + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'gpt-3.5-turbo', + 'gen_ai.request.temperature': 0.7, + 'gen_ai.response.model': 'gpt-3.5-turbo', + 'gen_ai.response.id': 'chatcmpl-mock123', + 'gen_ai.response.finish_reasons': '["stop"]', + 'gen_ai.usage.input_tokens': 10, + 'gen_ai.usage.output_tokens': 15, + 'gen_ai.usage.total_tokens': 25, + 'openai.response.id': 'chatcmpl-mock123', + 'openai.response.model': 'gpt-3.5-turbo', + 'openai.response.timestamp': '2023-03-01T06:31:28.000Z', + 'openai.usage.completion_tokens': 15, + 'openai.usage.prompt_tokens': 10, + }, + op: 'gen_ai.chat', + origin: 'auto.ai.openai', + status: 'ok', + }, + }, + }, + }) + .start() + .completed(); + }); + }); + createEsmAndCjsTests( __dirname, 'truncation/scenario-message-truncation-completions.mjs', diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/scenario-azure-openai.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/v6/scenario-azure-openai.mjs new file mode 100644 index 000000000000..6d519ae8b313 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/scenario-azure-openai.mjs @@ -0,0 +1,64 @@ +import express from 'express'; +import { AzureOpenAI } from 'openai'; + +function startMockOpenAiServer() { + const app = express(); + app.use(express.json()); + + app.post('/azureopenai/deployments/:model/chat/completions', (req, res) => { + res.send({ + id: 'chatcmpl-mock123', + object: 'chat.completion', + created: 1677652288, + model: req.body.model, + system_fingerprint: 'fp_44709d6fcb', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'Hello from OpenAI mock!', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 15, + total_tokens: 25, + }, + }); + }); + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const server = await startMockOpenAiServer(); + + const client = new AzureOpenAI({ + baseURL: `http://localhost:${server.address().port}/azureopenai`, + apiKey: 'mock-api-key', + apiVersion: '2024-02-15-preview', + }); + + const response = await client.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [ + { role: 'system', content: 'You are a helpful assistant.' }, + { role: 'user', content: 'What is the capital of France?' }, + ], + temperature: 0.7, + max_tokens: 100, + }); + + // eslint-disable-next-line no-console + console.log(JSON.stringify(response)); + + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts index 053f3066a1b0..4929325c6790 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts @@ -562,4 +562,62 @@ describe('OpenAI integration (V6)', () => { }, }, ); + + createEsmAndCjsTests( + __dirname, + 'scenario-azure-openai.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('it works with Azure OpenAI (v6)', async () => { + await createRunner() + // First the span that our mock express server is emitting, unrelated to this test + .expect({ + transaction: { + transaction: 'POST /azureopenai/deployments/:model/chat/completions', + }, + }) + .expect({ + transaction: { + transaction: 'chat gpt-3.5-turbo', + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'gen_ai.operation.name': 'chat', + 'sentry.op': 'gen_ai.chat', + 'sentry.origin': 'auto.ai.openai', + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'gpt-3.5-turbo', + 'gen_ai.request.temperature': 0.7, + 'gen_ai.response.model': 'gpt-3.5-turbo', + 'gen_ai.response.id': 'chatcmpl-mock123', + 'gen_ai.response.finish_reasons': '["stop"]', + 'gen_ai.usage.input_tokens': 10, + 'gen_ai.usage.output_tokens': 15, + 'gen_ai.usage.total_tokens': 25, + 'openai.response.id': 'chatcmpl-mock123', + 'openai.response.model': 'gpt-3.5-turbo', + 'openai.response.timestamp': '2023-03-01T06:31:28.000Z', + 'openai.usage.completion_tokens': 15, + 'openai.usage.prompt_tokens': 10, + }, + op: 'gen_ai.chat', + origin: 'auto.ai.openai', + status: 'ok', + }, + }, + }, + }) + .start() + .completed(); + }); + }, + { + additionalDependencies: { + openai: '6.0.0', + express: 'latest', + }, + }, + ); }); diff --git a/packages/node/src/integrations/tracing/openai/instrumentation.ts b/packages/node/src/integrations/tracing/openai/instrumentation.ts index e0682185ff0a..b1a577f9a5f4 100644 --- a/packages/node/src/integrations/tracing/openai/instrumentation.ts +++ b/packages/node/src/integrations/tracing/openai/instrumentation.ts @@ -25,6 +25,7 @@ export interface OpenAiIntegration extends Integration { interface PatchedModuleExports { [key: string]: unknown; OpenAI: abstract new (...args: unknown[]) => OpenAiClient; + AzureOpenAI?: abstract new (...args: unknown[]) => OpenAiClient; } /** @@ -56,10 +57,23 @@ export class SentryOpenAiInstrumentation extends InstrumentationBase Date: Mon, 24 Nov 2025 09:11:08 +0000 Subject: [PATCH 23/32] ref(react): Add more guarding against wildcards in lazy route transactions (#18155) Building on top of #17962 Added a few more checks to make sure non-resolved (wildcard) routes are not reported in lazy route pageloads / navigations. - Improved `patchSpanEnd` with a user-configurable wait timeout for potentially slow route resolution. Named this option as `lazyRouteTimeout` and it's defaulted as `idleTimeout` * 3. It may conditionally delay reporting (if the route resolution is still not done by the end of the timeout), but will prevent prematurely sent lazy-route transactions inside that window. - Added extra checks on `updateNavigationSpan` and `handleNavigation` for whether any wildcard still exists in a lazy-route, so they are still marked as open to full resolution. We keep track of pending lazy-route resolutions inside `pendingLazyRouteLoads` - Added a final attempt to update the transaction name with fully-resolved route when the pending resolution is done. Any of these should not affect the behaviour of non-lazy route usage --------- Co-authored-by: Sigrid <32902192+s1gr1d@users.noreply.github.com> --- .../react-router-7-lazy-routes/src/index.tsx | 63 +- .../src/pages/Deep.tsx | 12 + .../src/pages/DelayedLazyRoute.tsx | 50 + .../src/pages/Index.tsx | 12 + .../src/pages/deep/Level1Routes.tsx | 11 + .../src/pages/deep/Level2Routes.tsx | 14 + .../src/pages/deep/Level3.tsx | 13 + .../tests/timeout-behaviour.test.ts | 126 +++ .../tests/transactions.test.ts | 295 ++++++ .../src/reactrouter-compat-utils/index.ts | 1 + .../instrumentation.tsx | 846 ++++++++++------ .../src/reactrouter-compat-utils/utils.ts | 5 + .../instrumentation.test.tsx | 943 +++++++++++++++++- .../reactrouter-compat-utils/utils.test.ts | 35 + 14 files changed, 2114 insertions(+), 312 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/Deep.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/DelayedLazyRoute.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level1Routes.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level2Routes.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level3.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/timeout-behaviour.test.ts diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx index 521048fd18f4..7787b60be398 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/react'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { - Navigate, PatchRoutesOnNavigationFunction, RouterProvider, createBrowserRouter, @@ -12,6 +11,49 @@ import { useNavigationType, } from 'react-router-dom'; import Index from './pages/Index'; +import Deep from './pages/Deep'; + +function getRuntimeConfig(): { lazyRouteTimeout?: number; idleTimeout?: number } { + if (typeof window === 'undefined') { + return {}; + } + + try { + const url = new URL(window.location.href); + const timeoutParam = url.searchParams.get('timeout'); + const idleTimeoutParam = url.searchParams.get('idleTimeout'); + + let lazyRouteTimeout: number | undefined = undefined; + if (timeoutParam) { + if (timeoutParam === 'Infinity') { + lazyRouteTimeout = Infinity; + } else { + const parsed = parseInt(timeoutParam, 10); + if (!isNaN(parsed)) { + lazyRouteTimeout = parsed; + } + } + } + + let idleTimeout: number | undefined = undefined; + if (idleTimeoutParam) { + const parsed = parseInt(idleTimeoutParam, 10); + if (!isNaN(parsed)) { + idleTimeout = parsed; + } + } + + return { + lazyRouteTimeout, + idleTimeout, + }; + } catch (error) { + console.warn('Failed to read runtime config, falling back to defaults', error); + return {}; + } +} + +const runtimeConfig = getRuntimeConfig(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions @@ -25,6 +67,8 @@ Sentry.init({ matchRoutes, trackFetchStreamPerformance: true, enableAsyncRouteHandlers: true, + lazyRouteTimeout: runtimeConfig.lazyRouteTimeout, + idleTimeout: runtimeConfig.idleTimeout, }), ], // We recommend adjusting this value in production, or using tracesSampler @@ -66,8 +110,21 @@ const router = sentryCreateBrowserRouter( element: <>Hello World, }, { - path: '*', - element: , + path: '/delayed-lazy/:id', + lazy: async () => { + // Simulate slow lazy route loading (400ms delay) + await new Promise(resolve => setTimeout(resolve, 400)); + return { + Component: (await import('./pages/DelayedLazyRoute')).default, + }; + }, + }, + { + path: '/deep', + element: , + handle: { + lazyChildren: () => import('./pages/deep/Level1Routes').then(module => module.level2Routes), + }, }, ], { diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/Deep.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/Deep.tsx new file mode 100644 index 000000000000..c68f7b781e77 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/Deep.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; + +export default function Deep() { + return ( +
+

Deep Route Root

+

You are at the deep route root

+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/DelayedLazyRoute.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/DelayedLazyRoute.tsx new file mode 100644 index 000000000000..41e5ba5463be --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/DelayedLazyRoute.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Link, useParams, useLocation, useSearchParams } from 'react-router-dom'; + +const DelayedLazyRoute = () => { + const { id } = useParams<{ id: string }>(); + const location = useLocation(); + const [searchParams] = useSearchParams(); + const view = searchParams.get('view') || 'none'; + const source = searchParams.get('source') || 'none'; + + return ( +
+

Delayed Lazy Route

+

ID: {id}

+

{location.pathname}

+ +

{location.hash}

+

View: {view}

+

Source: {source}

+ + +
+ ); +}; + +export default DelayedLazyRoute; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/Index.tsx index 3053aa57b887..21b965f571f3 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/Index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/Index.tsx @@ -19,6 +19,18 @@ const Index = () => { Navigate to Long Running Lazy Route +
+ + Navigate to Delayed Lazy Parameterized Route + +
+ + Navigate to Delayed Lazy with Query Param + +
+ + Navigate to Deep Nested Route (3 levels, 900ms total) + ); }; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level1Routes.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level1Routes.tsx new file mode 100644 index 000000000000..0e0887b8850b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level1Routes.tsx @@ -0,0 +1,11 @@ +// Delay: 300ms before module loads +await new Promise(resolve => setTimeout(resolve, 300)); + +export const level2Routes = [ + { + path: 'level2', + handle: { + lazyChildren: () => import('./Level2Routes').then(module => module.level3Routes), + }, + }, +]; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level2Routes.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level2Routes.tsx new file mode 100644 index 000000000000..43671e1b7eee --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level2Routes.tsx @@ -0,0 +1,14 @@ +// Delay: 300ms before module loads +await new Promise(resolve => setTimeout(resolve, 300)); + +export const level3Routes = [ + { + path: 'level3/:id', + lazy: async () => { + await new Promise(resolve => setTimeout(resolve, 300)); + return { + Component: (await import('./Level3')).default, + }; + }, + }, +]; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level3.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level3.tsx new file mode 100644 index 000000000000..e44ecc7da655 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/deep/Level3.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +export default function Level3() { + const { id } = useParams(); + return ( +
+

Level 3 Deep Route

+

Deeply nested route loaded!

+

ID: {id}

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/timeout-behaviour.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/timeout-behaviour.test.ts new file mode 100644 index 000000000000..281ebc88e52c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/timeout-behaviour.test.ts @@ -0,0 +1,126 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('lazyRouteTimeout: Routes load within timeout window', async ({ page }) => { + const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction.includes('deep') + ); + }); + + // Route takes ~900ms, timeout allows 1050ms (50 + 1000) + // Routes will load in time → parameterized name + await page.goto('/?idleTimeout=50&timeout=1000'); + + const navigationLink = page.locator('id=navigation-to-deep'); + await expect(navigationLink).toBeVisible(); + await navigationLink.click(); + + const event = await transactionPromise; + + // Should get full parameterized route + expect(event.transaction).toBe('/deep/level2/level3/:id'); + expect(event.contexts?.trace?.data?.['sentry.source']).toBe('route'); + expect(event.contexts?.trace?.data?.['sentry.idle_span_finish_reason']).toBe('idleTimeout'); +}); + +test('lazyRouteTimeout: Infinity timeout always waits for routes', async ({ page }) => { + const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction.includes('deep') + ); + }); + + // Infinity timeout → waits as long as possible (capped at finalTimeout to prevent indefinite hangs) + await page.goto('/?idleTimeout=50&timeout=Infinity'); + + const navigationLink = page.locator('id=navigation-to-deep'); + await expect(navigationLink).toBeVisible(); + await navigationLink.click(); + + const event = await transactionPromise; + + // Should wait for routes to load (up to finalTimeout) and get full route + expect(event.transaction).toBe('/deep/level2/level3/:id'); + expect(event.contexts?.trace?.data?.['sentry.source']).toBe('route'); + expect(event.contexts?.trace?.data?.['sentry.idle_span_finish_reason']).toBe('idleTimeout'); +}); + +test('idleTimeout: Captures all activity with increased timeout', async ({ page }) => { + const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction.includes('deep') + ); + }); + + // High idleTimeout (5000ms) ensures transaction captures all lazy loading activity + await page.goto('/?idleTimeout=5000'); + + const navigationLink = page.locator('id=navigation-to-deep'); + await expect(navigationLink).toBeVisible(); + await navigationLink.click(); + + const event = await transactionPromise; + + expect(event.transaction).toBe('/deep/level2/level3/:id'); + expect(event.contexts?.trace?.data?.['sentry.source']).toBe('route'); + expect(event.contexts?.trace?.data?.['sentry.idle_span_finish_reason']).toBe('idleTimeout'); + + // Transaction should wait for full idle timeout (5+ seconds) + const duration = event.timestamp! - event.start_timestamp; + expect(duration).toBeGreaterThan(5.0); + expect(duration).toBeLessThan(7.0); +}); + +test('idleTimeout: Finishes prematurely with low timeout', async ({ page }) => { + const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction.includes('deep') + ); + }); + + // Very low idleTimeout (50ms) and lazyRouteTimeout (100ms) + // Transaction finishes quickly, but still gets parameterized route name + await page.goto('/?idleTimeout=50&timeout=100'); + + const navigationLink = page.locator('id=navigation-to-deep'); + await expect(navigationLink).toBeVisible(); + await navigationLink.click(); + + const event = await transactionPromise; + + expect(event.contexts?.trace?.data?.['sentry.idle_span_finish_reason']).toBe('idleTimeout'); + expect(event.transaction).toBe('/deep/level2/level3/:id'); + expect(event.contexts?.trace?.data?.['sentry.source']).toBe('route'); + + // Transaction should finish quickly (< 200ms) + const duration = event.timestamp! - event.start_timestamp; + expect(duration).toBeLessThan(0.2); +}); + +test('idleTimeout: Pageload on deeply nested route', async ({ page }) => { + const pageloadPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'pageload' && + transactionEvent.transaction.includes('deep') + ); + }); + + // Direct pageload to deeply nested route (not navigation) + await page.goto('/deep/level2/level3/12345'); + + const pageloadEvent = await pageloadPromise; + + expect(pageloadEvent.transaction).toBe('/deep/level2/level3/:id'); + expect(pageloadEvent.contexts?.trace?.data?.['sentry.source']).toBe('route'); + expect(pageloadEvent.contexts?.trace?.data?.['sentry.idle_span_finish_reason']).toBe('idleTimeout'); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts index e5b9f35042ed..ce8137d7f686 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts @@ -588,3 +588,298 @@ test('Creates separate transactions for rapid consecutive navigations', async ({ expect(secondSpanId).not.toBe(thirdSpanId); expect(firstSpanId).not.toBe(thirdSpanId); }); + +test('Creates pageload transaction with parameterized route for delayed lazy route', async ({ page }) => { + const pageloadPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'pageload' && + transactionEvent.transaction === '/delayed-lazy/:id' + ); + }); + + await page.goto('/delayed-lazy/123'); + + const pageloadEvent = await pageloadPromise; + + const delayedReady = page.locator('id=delayed-lazy-ready'); + await expect(delayedReady).toBeVisible(); + await expect(page.locator('id=delayed-lazy-id')).toHaveText('ID: 123'); + await expect(page.locator('id=delayed-lazy-path')).toHaveText('/delayed-lazy/123'); + + expect(pageloadEvent.transaction).toBe('/delayed-lazy/:id'); + expect(pageloadEvent.contexts?.trace?.op).toBe('pageload'); + expect(pageloadEvent.contexts?.trace?.data?.['sentry.source']).toBe('route'); +}); + +test('Creates navigation transaction with parameterized route for delayed lazy route', async ({ page }) => { + await page.goto('/'); + + const navigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/delayed-lazy/:id' + ); + }); + + const navigationLink = page.locator('id=navigation-to-delayed-lazy'); + await expect(navigationLink).toBeVisible(); + await navigationLink.click(); + + const navigationEvent = await navigationPromise; + + const delayedReady = page.locator('id=delayed-lazy-ready'); + await expect(delayedReady).toBeVisible(); + await expect(page.locator('id=delayed-lazy-id')).toHaveText('ID: 123'); + await expect(page.locator('id=delayed-lazy-path')).toHaveText('/delayed-lazy/123'); + + expect(navigationEvent.transaction).toBe('/delayed-lazy/:id'); + expect(navigationEvent.contexts?.trace?.op).toBe('navigation'); + expect(navigationEvent.contexts?.trace?.data?.['sentry.source']).toBe('route'); +}); + +test('Creates navigation transaction when navigating with query parameters from home to route', async ({ page }) => { + await page.goto('/'); + + // Navigate from / to /delayed-lazy/123?source=homepage + // This should create a navigation transaction with the parameterized route name + const navigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/delayed-lazy/:id' + ); + }); + + const navigationLink = page.locator('id=navigation-to-delayed-lazy-with-query'); + await expect(navigationLink).toBeVisible(); + await navigationLink.click(); + + const navigationEvent = await navigationPromise; + + const delayedReady = page.locator('id=delayed-lazy-ready'); + await expect(delayedReady).toBeVisible(); + await expect(page.locator('id=delayed-lazy-id')).toHaveText('ID: 123'); + await expect(page.locator('id=delayed-lazy-path')).toHaveText('/delayed-lazy/123'); + await expect(page.locator('id=delayed-lazy-search')).toHaveText('?source=homepage'); + await expect(page.locator('id=delayed-lazy-source')).toHaveText('Source: homepage'); + + // Verify the navigation transaction has the correct parameterized route name + // Query parameters should NOT affect the transaction name (still /delayed-lazy/:id) + expect(navigationEvent.transaction).toBe('/delayed-lazy/:id'); + expect(navigationEvent.contexts?.trace?.op).toBe('navigation'); + expect(navigationEvent.contexts?.trace?.data?.['sentry.source']).toBe('route'); + expect(navigationEvent.contexts?.trace?.status).toBe('ok'); +}); + +test('Creates separate navigation transaction when changing only query parameters on same route', async ({ page }) => { + await page.goto('/delayed-lazy/123'); + + // Wait for the page to fully load + const delayedReady = page.locator('id=delayed-lazy-ready'); + await expect(delayedReady).toBeVisible(); + + // Navigate from /delayed-lazy/123 to /delayed-lazy/123?view=detailed + // This is a query-only change on the same route + const navigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/delayed-lazy/:id' + ); + }); + + const queryLink = page.locator('id=link-to-query-view-detailed'); + await expect(queryLink).toBeVisible(); + await queryLink.click(); + + const navigationEvent = await navigationPromise; + + // Verify query param was updated + await expect(page.locator('id=delayed-lazy-search')).toHaveText('?view=detailed'); + await expect(page.locator('id=delayed-lazy-view')).toHaveText('View: detailed'); + + // Query-only navigation should create a navigation transaction + expect(navigationEvent.transaction).toBe('/delayed-lazy/:id'); + expect(navigationEvent.contexts?.trace?.op).toBe('navigation'); + expect(navigationEvent.contexts?.trace?.data?.['sentry.source']).toBe('route'); + expect(navigationEvent.contexts?.trace?.status).toBe('ok'); +}); + +test('Creates separate navigation transactions for multiple query parameter changes', async ({ page }) => { + await page.goto('/delayed-lazy/123'); + + const delayedReady = page.locator('id=delayed-lazy-ready'); + await expect(delayedReady).toBeVisible(); + + // First query change: /delayed-lazy/123 -> /delayed-lazy/123?view=detailed + const firstNavigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/delayed-lazy/:id' + ); + }); + + const firstQueryLink = page.locator('id=link-to-query-view-detailed'); + await expect(firstQueryLink).toBeVisible(); + await firstQueryLink.click(); + + const firstNavigationEvent = await firstNavigationPromise; + const firstTraceId = firstNavigationEvent.contexts?.trace?.trace_id; + + await expect(page.locator('id=delayed-lazy-view')).toHaveText('View: detailed'); + + // Second query change: /delayed-lazy/123?view=detailed -> /delayed-lazy/123?view=list + const secondNavigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/delayed-lazy/:id' && + transactionEvent.contexts?.trace?.trace_id !== firstTraceId + ); + }); + + const secondQueryLink = page.locator('id=link-to-query-view-list'); + await expect(secondQueryLink).toBeVisible(); + await secondQueryLink.click(); + + const secondNavigationEvent = await secondNavigationPromise; + const secondTraceId = secondNavigationEvent.contexts?.trace?.trace_id; + + await expect(page.locator('id=delayed-lazy-view')).toHaveText('View: list'); + + // Both navigations should have created separate transactions + expect(firstNavigationEvent.transaction).toBe('/delayed-lazy/:id'); + expect(firstNavigationEvent.contexts?.trace?.op).toBe('navigation'); + expect(secondNavigationEvent.transaction).toBe('/delayed-lazy/:id'); + expect(secondNavigationEvent.contexts?.trace?.op).toBe('navigation'); + + // Trace IDs should be different (separate transactions) + expect(firstTraceId).toBeDefined(); + expect(secondTraceId).toBeDefined(); + expect(firstTraceId).not.toBe(secondTraceId); +}); + +test('Creates navigation transaction when changing only hash on same route', async ({ page }) => { + await page.goto('/delayed-lazy/123'); + + const delayedReady = page.locator('id=delayed-lazy-ready'); + await expect(delayedReady).toBeVisible(); + + // Navigate from /delayed-lazy/123 to /delayed-lazy/123#section1 + // This is a hash-only change on the same route + const navigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/delayed-lazy/:id' + ); + }); + + const hashLink = page.locator('id=link-to-hash-section1'); + await expect(hashLink).toBeVisible(); + await hashLink.click(); + + const navigationEvent = await navigationPromise; + + // Verify hash was updated + await expect(page.locator('id=delayed-lazy-hash')).toHaveText('#section1'); + + // Hash-only navigation should create a navigation transaction + expect(navigationEvent.transaction).toBe('/delayed-lazy/:id'); + expect(navigationEvent.contexts?.trace?.op).toBe('navigation'); + expect(navigationEvent.contexts?.trace?.data?.['sentry.source']).toBe('route'); + expect(navigationEvent.contexts?.trace?.status).toBe('ok'); +}); + +test('Creates separate navigation transactions for multiple hash changes', async ({ page }) => { + await page.goto('/delayed-lazy/123'); + + const delayedReady = page.locator('id=delayed-lazy-ready'); + await expect(delayedReady).toBeVisible(); + + // First hash change: /delayed-lazy/123 -> /delayed-lazy/123#section1 + const firstNavigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/delayed-lazy/:id' + ); + }); + + const firstHashLink = page.locator('id=link-to-hash-section1'); + await expect(firstHashLink).toBeVisible(); + await firstHashLink.click(); + + const firstNavigationEvent = await firstNavigationPromise; + const firstTraceId = firstNavigationEvent.contexts?.trace?.trace_id; + + await expect(page.locator('id=delayed-lazy-hash')).toHaveText('#section1'); + + // Second hash change: /delayed-lazy/123#section1 -> /delayed-lazy/123#section2 + const secondNavigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/delayed-lazy/:id' && + transactionEvent.contexts?.trace?.trace_id !== firstTraceId + ); + }); + + const secondHashLink = page.locator('id=link-to-hash-section2'); + await expect(secondHashLink).toBeVisible(); + await secondHashLink.click(); + + const secondNavigationEvent = await secondNavigationPromise; + const secondTraceId = secondNavigationEvent.contexts?.trace?.trace_id; + + await expect(page.locator('id=delayed-lazy-hash')).toHaveText('#section2'); + + // Both navigations should have created separate transactions + expect(firstNavigationEvent.transaction).toBe('/delayed-lazy/:id'); + expect(firstNavigationEvent.contexts?.trace?.op).toBe('navigation'); + expect(secondNavigationEvent.transaction).toBe('/delayed-lazy/:id'); + expect(secondNavigationEvent.contexts?.trace?.op).toBe('navigation'); + + // Trace IDs should be different (separate transactions) + expect(firstTraceId).toBeDefined(); + expect(secondTraceId).toBeDefined(); + expect(firstTraceId).not.toBe(secondTraceId); +}); + +test('Creates navigation transaction when changing both query and hash on same route', async ({ page }) => { + await page.goto('/delayed-lazy/123?view=list'); + + const delayedReady = page.locator('id=delayed-lazy-ready'); + await expect(delayedReady).toBeVisible(); + await expect(page.locator('id=delayed-lazy-view')).toHaveText('View: list'); + + // Navigate from /delayed-lazy/123?view=list to /delayed-lazy/123?view=grid#results + // This changes both query and hash + const navigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/delayed-lazy/:id' + ); + }); + + const queryAndHashLink = page.locator('id=link-to-query-and-hash'); + await expect(queryAndHashLink).toBeVisible(); + await queryAndHashLink.click(); + + const navigationEvent = await navigationPromise; + + // Verify both query and hash were updated + await expect(page.locator('id=delayed-lazy-search')).toHaveText('?view=grid'); + await expect(page.locator('id=delayed-lazy-hash')).toHaveText('#results'); + await expect(page.locator('id=delayed-lazy-view')).toHaveText('View: grid'); + + // Combined query + hash navigation should create a navigation transaction + expect(navigationEvent.transaction).toBe('/delayed-lazy/:id'); + expect(navigationEvent.contexts?.trace?.op).toBe('navigation'); + expect(navigationEvent.contexts?.trace?.data?.['sentry.source']).toBe('route'); + expect(navigationEvent.contexts?.trace?.status).toBe('ok'); +}); diff --git a/packages/react/src/reactrouter-compat-utils/index.ts b/packages/react/src/reactrouter-compat-utils/index.ts index c2b56ec446fb..bb91ba8d3072 100644 --- a/packages/react/src/reactrouter-compat-utils/index.ts +++ b/packages/react/src/reactrouter-compat-utils/index.ts @@ -25,6 +25,7 @@ export { pathEndsWithWildcard, pathIsWildcardAndHasChildren, getNumberOfUrlSegments, + transactionNameHasWildcard, } from './utils'; // Lazy route exports diff --git a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx index 235b207ed9a0..6e19b9021ba5 100644 --- a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx +++ b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx @@ -41,59 +41,118 @@ import type { UseRoutes, } from '../types'; import { checkRouteForAsyncHandler } from './lazy-routes'; -import { initializeRouterUtils, resolveRouteNameAndSource } from './utils'; +import { initializeRouterUtils, resolveRouteNameAndSource, transactionNameHasWildcard } from './utils'; let _useEffect: UseEffect; let _useLocation: UseLocation; let _useNavigationType: UseNavigationType; let _createRoutesFromChildren: CreateRoutesFromChildren; let _matchRoutes: MatchRoutes; + let _enableAsyncRouteHandlers: boolean = false; +let _lazyRouteTimeout = 3000; const CLIENTS_WITH_INSTRUMENT_NAVIGATION = new WeakSet(); +// Prevents duplicate spans when router.subscribe fires multiple times +const activeNavigationSpans = new WeakMap< + Client, + { span: Span; routeName: string; pathname: string; locationKey: string; isPlaceholder?: boolean } +>(); + +// Exported for testing only +export const allRoutes = new Set(); + +// Tracks lazy route loads to wait before finalizing span names +const pendingLazyRouteLoads = new WeakMap>>(); + /** - * Tracks last navigation per client to prevent duplicate spans in cross-usage scenarios. - * Entry persists until the navigation span ends, allowing cross-usage detection during delayed wrapper execution. + * Schedules a callback using requestAnimationFrame when available (browser), + * or falls back to setTimeout for SSR environments (Node.js, createMemoryRouter tests). */ -const LAST_NAVIGATION_PER_CLIENT = new WeakMap(); +function scheduleCallback(callback: () => void): number { + if (WINDOW?.requestAnimationFrame) { + return WINDOW.requestAnimationFrame(callback); + } + return setTimeout(callback, 0) as unknown as number; +} -export function addResolvedRoutesToParent(resolvedRoutes: RouteObject[], parentRoute: RouteObject): void { - const existingChildren = parentRoute.children || []; +/** + * Cancels a scheduled callback, handling both RAF (browser) and timeout (SSR) IDs. + */ +function cancelScheduledCallback(id: number): void { + if (WINDOW?.cancelAnimationFrame) { + WINDOW.cancelAnimationFrame(id); + } else { + clearTimeout(id); + } +} - const newRoutes = resolvedRoutes.filter( - newRoute => - !existingChildren.some( - existing => - existing === newRoute || - (newRoute.path && existing.path === newRoute.path) || - (newRoute.id && existing.id === newRoute.id), - ), - ); +/** + * Computes location key for duplicate detection. Normalizes undefined/null to empty strings. + * Exported for testing. + */ +export function computeLocationKey(location: Location): string { + return `${location.pathname}${location.search || ''}${location.hash || ''}`; +} - if (newRoutes.length > 0) { - parentRoute.children = [...existingChildren, ...newRoutes]; - } +/** + * Checks if a route name is parameterized (contains route parameters like :id or wildcards like *) + * vs a raw URL path. + */ +function isParameterizedRoute(routeName: string): boolean { + return routeName.includes(':') || routeName.includes('*'); } /** - * Determines if a navigation should be handled based on router state. - * Only handles: - * - PUSH navigations (always) - * - POP navigations (only after initial pageload is complete) - * - When router state is 'idle' (not 'loading' or 'submitting') + * Determines if a navigation should be skipped as a duplicate, and if an existing span should be updated. + * Exported for testing. * - * During 'loading' or 'submitting', state.location may still have the old pathname, - * which would cause us to create a span for the wrong route. + * @returns An object with: + * - skip: boolean - Whether to skip creating a new span + * - shouldUpdate: boolean - Whether to update the existing span name (wildcard upgrade) */ -function shouldHandleNavigation( - state: { historyAction: string; navigation: { state: string } }, - isInitialPageloadComplete: boolean, -): boolean { - return ( - (state.historyAction === 'PUSH' || (state.historyAction === 'POP' && isInitialPageloadComplete)) && - state.navigation.state === 'idle' - ); +export function shouldSkipNavigation( + trackedNav: + | { span: Span; routeName: string; pathname: string; locationKey: string; isPlaceholder?: boolean } + | undefined, + locationKey: string, + proposedName: string, + spanHasEnded: boolean, +): { skip: boolean; shouldUpdate: boolean } { + if (!trackedNav) { + return { skip: false, shouldUpdate: false }; + } + + // Check if this is a duplicate navigation (same location) + // 1. If it's a placeholder, it's always a duplicate (we're waiting for the real one) + // 2. If it's a real span, it's a duplicate only if it hasn't ended yet + const isDuplicate = trackedNav.locationKey === locationKey && (trackedNav.isPlaceholder || !spanHasEnded); + + if (isDuplicate) { + // Check if we should update the span name with a better route + // Allow updates if: + // 1. Current has wildcard and new doesn't (wildcard → parameterized upgrade) + // 2. Current is raw path and new is parameterized (raw → parameterized upgrade) + // 3. New name is different and more specific (longer, indicating nested routes resolved) + const currentHasWildcard = !!trackedNav.routeName && transactionNameHasWildcard(trackedNav.routeName); + const proposedHasWildcard = transactionNameHasWildcard(proposedName); + const currentIsParameterized = !!trackedNav.routeName && isParameterizedRoute(trackedNav.routeName); + const proposedIsParameterized = isParameterizedRoute(proposedName); + + const isWildcardUpgrade = currentHasWildcard && !proposedHasWildcard; + const isRawToParameterized = !currentIsParameterized && proposedIsParameterized; + const isMoreSpecific = + proposedName !== trackedNav.routeName && + proposedName.length > (trackedNav.routeName?.length || 0) && + !proposedHasWildcard; + + const shouldUpdate = !!(trackedNav.routeName && (isWildcardUpgrade || isRawToParameterized || isMoreSpecific)); + + return { skip: true, shouldUpdate }; + } + + return { skip: false, shouldUpdate: false }; } export interface ReactRouterOptions { @@ -116,13 +175,58 @@ export interface ReactRouterOptions { * @default false */ enableAsyncRouteHandlers?: boolean; + + /** + * Maximum time (in milliseconds) to wait for lazy routes to load before finalizing span names. + * + * - Set to `0` to not wait at all (immediate finalization) + * - Set to `Infinity` to wait as long as possible (capped at `finalTimeout` to prevent indefinite hangs) + * - Negative values will fall back to the default + * + * Defaults to 3× the configured `idleTimeout` (default: 3000ms). + * + * @default idleTimeout * 3 + */ + lazyRouteTimeout?: number; } type V6CompatibleVersion = '6' | '7'; -// Keeping as a global variable for cross-usage in multiple functions -// only exported for testing purposes -export const allRoutes = new Set(); +export function addResolvedRoutesToParent(resolvedRoutes: RouteObject[], parentRoute: RouteObject): void { + const existingChildren = parentRoute.children || []; + + const newRoutes = resolvedRoutes.filter( + newRoute => + !existingChildren.some( + existing => + existing === newRoute || + (newRoute.path && existing.path === newRoute.path) || + (newRoute.id && existing.id === newRoute.id), + ), + ); + + if (newRoutes.length > 0) { + parentRoute.children = [...existingChildren, ...newRoutes]; + } +} + +/** Registers a pending lazy route load promise for a span. */ +function trackLazyRouteLoad(span: Span, promise: Promise): void { + let promises = pendingLazyRouteLoads.get(span); + if (!promises) { + promises = new Set(); + pendingLazyRouteLoads.set(span, promises); + } + promises.add(promise); + + // Clean up when promise resolves/rejects + promise.finally(() => { + const currentPromises = pendingLazyRouteLoads.get(span); + if (currentPromises) { + currentPromises.delete(promise); + } + }); +} /** * Processes resolved routes by adding them to allRoutes and checking for nested async handlers. @@ -188,13 +292,14 @@ export function updateNavigationSpan( forceUpdate = false, matchRoutes: MatchRoutes, ): void { - // Check if this span has already been named to avoid multiple updates - // But allow updates if this is a forced update (e.g., when lazy routes are loaded) - const hasBeenNamed = - !forceUpdate && (activeRootSpan as { __sentry_navigation_name_set__?: boolean })?.__sentry_navigation_name_set__; + const spanJson = spanToJSON(activeRootSpan); + const currentName = spanJson.description; - if (!hasBeenNamed) { - // Get fresh branches for the current location with all loaded routes + const hasBeenNamed = (activeRootSpan as { __sentry_navigation_name_set__?: boolean })?.__sentry_navigation_name_set__; + const currentNameHasWildcard = currentName && transactionNameHasWildcard(currentName); + const shouldUpdate = !hasBeenNamed || forceUpdate || currentNameHasWildcard; + + if (shouldUpdate && !spanJson.timestamp) { const currentBranches = matchRoutes(allRoutes, location); const [name, source] = resolveRouteNameAndSource( location, @@ -204,22 +309,105 @@ export function updateNavigationSpan( '', ); - // Only update if we have a valid name and the span hasn't finished - const spanJson = spanToJSON(activeRootSpan); - if (name && !spanJson.timestamp) { + const currentSource = spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + const isImprovement = + name && + (!currentName || // No current name - always set + (!hasBeenNamed && (currentSource !== 'route' || source === 'route')) || // Not finalized - allow unless downgrading route→url + (currentSource !== 'route' && source === 'route') || // URL → route upgrade + (currentSource === 'route' && source === 'route' && currentNameHasWildcard)); // Route → better route (only if current has wildcard) + if (isImprovement) { activeRootSpan.updateName(name); activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); - // Mark this span as having its name set to prevent future updates - addNonEnumerableProperty( - activeRootSpan as { __sentry_navigation_name_set__?: boolean }, - '__sentry_navigation_name_set__', - true, - ); + // Only mark as finalized for non-wildcard route names (allows URL→route upgrades). + if (!transactionNameHasWildcard(name) && source === 'route') { + addNonEnumerableProperty( + activeRootSpan as { __sentry_navigation_name_set__?: boolean }, + '__sentry_navigation_name_set__', + true, + ); + } } } } +function setupRouterSubscription( + router: Router, + routes: RouteObject[], + version: V6CompatibleVersion, + basename: string | undefined, + activeRootSpan: Span | undefined, +): void { + let isInitialPageloadComplete = false; + let hasSeenPageloadSpan = !!activeRootSpan && spanToJSON(activeRootSpan).op === 'pageload'; + let hasSeenPopAfterPageload = false; + let scheduledNavigationHandler: number | null = null; + let lastHandledPathname: string | null = null; + + router.subscribe((state: RouterState) => { + if (!isInitialPageloadComplete) { + const currentRootSpan = getActiveRootSpan(); + const isCurrentlyInPageload = currentRootSpan && spanToJSON(currentRootSpan).op === 'pageload'; + + if (isCurrentlyInPageload) { + hasSeenPageloadSpan = true; + } else if (hasSeenPageloadSpan) { + if (state.historyAction === 'POP' && !hasSeenPopAfterPageload) { + hasSeenPopAfterPageload = true; + } else { + isInitialPageloadComplete = true; + } + } + } + + const shouldHandleNavigation = + state.historyAction === 'PUSH' || (state.historyAction === 'POP' && isInitialPageloadComplete); + + if (shouldHandleNavigation) { + // Include search and hash to allow query/hash-only navigations + // Use computeLocationKey() to ensure undefined/null values are normalized to empty strings + const currentLocationKey = computeLocationKey(state.location); + const navigationHandler = (): void => { + // Prevent multiple calls for the same location within the same navigation cycle + if (lastHandledPathname === currentLocationKey) { + return; + } + lastHandledPathname = currentLocationKey; + scheduledNavigationHandler = null; + handleNavigation({ + location: state.location, + routes, + navigationType: state.historyAction, + version, + basename, + allRoutes: Array.from(allRoutes), + }); + }; + + if (state.navigation.state !== 'idle') { + // Navigation in progress - reset if location changed + if (lastHandledPathname !== currentLocationKey) { + lastHandledPathname = null; + } + // Cancel any previously scheduled handler to avoid duplicates + if (scheduledNavigationHandler !== null) { + cancelScheduledCallback(scheduledNavigationHandler); + } + scheduledNavigationHandler = scheduleCallback(navigationHandler); + } else { + // Navigation completed - cancel scheduled handler if any, then call immediately + if (scheduledNavigationHandler !== null) { + cancelScheduledCallback(scheduledNavigationHandler); + scheduledNavigationHandler = null; + } + navigationHandler(); + // Don't reset - next navigation cycle resets to prevent duplicates within same cycle. + } + } + }); +} + /** * Creates a wrapCreateBrowserRouter function that can be used with all React Router v6 compatible versions. */ @@ -242,30 +430,17 @@ export function createV6CompatibleWrapCreateBrowserRouter< return function (routes: RouteObject[], opts?: Record & { basename?: string }): TRouter { addRoutesToAllRoutes(routes); - // Check for async handlers that might contain sub-route declarations (only if enabled) if (_enableAsyncRouteHandlers) { for (const route of routes) { checkRouteForAsyncHandler(route, processResolvedRoutes); } } - // Wrap patchRoutesOnNavigation to detect when lazy routes are loaded const wrappedOpts = wrapPatchRoutesOnNavigation(opts); - const router = createRouterFunction(routes, wrappedOpts); const basename = opts?.basename; - const activeRootSpan = getActiveRootSpan(); - // Track whether we've completed the initial pageload to properly distinguish - // between POPs that occur during pageload vs. legitimate back/forward navigation. - let isInitialPageloadComplete = false; - let hasSeenPageloadSpan = !!activeRootSpan && spanToJSON(activeRootSpan).op === 'pageload'; - let hasSeenPopAfterPageload = false; - - // The initial load ends when `createBrowserRouter` is called. - // This is the earliest convenient time to update the transaction name. - // Callbacks to `router.subscribe` are not called for the initial load. if (router.state.historyAction === 'POP' && activeRootSpan) { updatePageloadTransaction({ activeRootSpan, @@ -276,38 +451,7 @@ export function createV6CompatibleWrapCreateBrowserRouter< }); } - router.subscribe((state: RouterState) => { - // Track pageload completion to distinguish POPs during pageload from legitimate back/forward navigation - if (!isInitialPageloadComplete) { - const currentRootSpan = getActiveRootSpan(); - const isCurrentlyInPageload = currentRootSpan && spanToJSON(currentRootSpan).op === 'pageload'; - - if (isCurrentlyInPageload) { - hasSeenPageloadSpan = true; - } else if (hasSeenPageloadSpan) { - // Pageload span was active but is now gone - pageload has completed - if (state.historyAction === 'POP' && !hasSeenPopAfterPageload) { - // Pageload ended: ignore the first POP after pageload - hasSeenPopAfterPageload = true; - } else { - // Pageload ended: either non-POP action or subsequent POP - isInitialPageloadComplete = true; - } - } - // If we haven't seen a pageload span yet, keep waiting (don't mark as complete) - } - - if (shouldHandleNavigation(state, isInitialPageloadComplete)) { - handleNavigation({ - location: state.location, - routes, - navigationType: state.historyAction, - version, - basename, - allRoutes: Array.from(allRoutes), - }); - } - }); + setupRouterSubscription(router, routes, version, basename, activeRootSpan); return router; }; @@ -342,14 +486,12 @@ export function createV6CompatibleWrapCreateMemoryRouter< ): TRouter { addRoutesToAllRoutes(routes); - // Check for async handlers that might contain sub-route declarations (only if enabled) if (_enableAsyncRouteHandlers) { for (const route of routes) { checkRouteForAsyncHandler(route, processResolvedRoutes); } } - // Wrap patchRoutesOnNavigation to detect when lazy routes are loaded const wrappedOpts = wrapPatchRoutesOnNavigation(opts, true); const router = createRouterFunction(routes, wrappedOpts); @@ -387,44 +529,7 @@ export function createV6CompatibleWrapCreateMemoryRouter< }); } - // Track whether we've completed the initial pageload to properly distinguish - // between POPs that occur during pageload vs. legitimate back/forward navigation. - let isInitialPageloadComplete = false; - let hasSeenPageloadSpan = !!memoryActiveRootSpan && spanToJSON(memoryActiveRootSpan).op === 'pageload'; - let hasSeenPopAfterPageload = false; - - router.subscribe((state: RouterState) => { - // Track pageload completion to distinguish POPs during pageload from legitimate back/forward navigation - if (!isInitialPageloadComplete) { - const currentRootSpan = getActiveRootSpan(); - const isCurrentlyInPageload = currentRootSpan && spanToJSON(currentRootSpan).op === 'pageload'; - - if (isCurrentlyInPageload) { - hasSeenPageloadSpan = true; - } else if (hasSeenPageloadSpan) { - // Pageload span was active but is now gone - pageload has completed - if (state.historyAction === 'POP' && !hasSeenPopAfterPageload) { - // Pageload ended: ignore the first POP after pageload - hasSeenPopAfterPageload = true; - } else { - // Pageload ended: either non-POP action or subsequent POP - isInitialPageloadComplete = true; - } - } - // If we haven't seen a pageload span yet, keep waiting (don't mark as complete) - } - - if (shouldHandleNavigation(state, isInitialPageloadComplete)) { - handleNavigation({ - location: state.location, - routes, - navigationType: state.historyAction, - version, - basename, - allRoutes: Array.from(allRoutes), - }); - } - }); + setupRouterSubscription(router, routes, version, basename, memoryActiveRootSpan); return router; }; @@ -449,6 +554,7 @@ export function createReactRouterV6CompatibleTracingIntegration( enableAsyncRouteHandlers = false, instrumentPageLoad = true, instrumentNavigation = true, + lazyRouteTimeout, } = options; return { @@ -456,6 +562,36 @@ export function createReactRouterV6CompatibleTracingIntegration( setup(client) { integration.setup(client); + const finalTimeout = options.finalTimeout ?? 30000; + const defaultMaxWait = (options.idleTimeout ?? 1000) * 3; + const configuredMaxWait = lazyRouteTimeout ?? defaultMaxWait; + + // Cap Infinity at finalTimeout to prevent indefinite hangs + if (configuredMaxWait === Infinity) { + _lazyRouteTimeout = finalTimeout; + DEBUG_BUILD && + debug.log( + '[React Router] lazyRouteTimeout set to Infinity, capping at finalTimeout:', + finalTimeout, + 'ms to prevent indefinite hangs', + ); + } else if (Number.isNaN(configuredMaxWait)) { + DEBUG_BUILD && + debug.warn('[React Router] lazyRouteTimeout must be a number, falling back to default:', defaultMaxWait); + _lazyRouteTimeout = defaultMaxWait; + } else if (configuredMaxWait < 0) { + DEBUG_BUILD && + debug.warn( + '[React Router] lazyRouteTimeout must be non-negative or Infinity, got:', + configuredMaxWait, + 'falling back to:', + defaultMaxWait, + ); + _lazyRouteTimeout = defaultMaxWait; + } else { + _lazyRouteTimeout = configuredMaxWait; + } + _useEffect = useEffect; _useLocation = useLocation; _useNavigationType = useNavigationType; @@ -530,6 +666,9 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio }); isMountRenderPass.current = false; } else { + // Note: Component-based routes don't support lazy route tracking via lazyRouteTimeout + // because React.lazy() loads happen at the component level, not the router level. + // Use createBrowserRouter with patchRoutesOnNavigation for lazy route tracking. handleNavigation({ location: normalizedLocation, routes, @@ -564,7 +703,8 @@ function wrapPatchRoutesOnNavigation( // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access const targetPath = (args as any)?.path; - // For browser router, wrap the patch function to update span during patching + const activeRootSpan = getActiveRootSpan(); + if (!isMemoryRouter) { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access const originalPatch = (args as any)?.patch; @@ -572,13 +712,13 @@ function wrapPatchRoutesOnNavigation( // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access (args as any).patch = (routeId: string, children: RouteObject[]) => { addRoutesToAllRoutes(children); - const activeRootSpan = getActiveRootSpan(); - if (activeRootSpan && (spanToJSON(activeRootSpan) as { op?: string }).op === 'navigation') { + const currentActiveRootSpan = getActiveRootSpan(); + if (currentActiveRootSpan && (spanToJSON(currentActiveRootSpan) as { op?: string }).op === 'navigation') { updateNavigationSpan( - activeRootSpan, + currentActiveRootSpan, { pathname: targetPath, search: '', hash: '', state: null, key: 'default' }, Array.from(allRoutes), - true, // forceUpdate = true since we're loading lazy routes + true, _matchRoutes, ); } @@ -587,102 +727,37 @@ function wrapPatchRoutesOnNavigation( } } - const result = await originalPatchRoutes(args); - - // Update navigation span after routes are patched - const activeRootSpan = getActiveRootSpan(); - if (activeRootSpan && (spanToJSON(activeRootSpan) as { op?: string }).op === 'navigation') { - // Determine pathname based on router type - let pathname: string | undefined; - if (isMemoryRouter) { - // For memory routers, only use targetPath - pathname = targetPath; - } else { - // For browser routers, use targetPath or fall back to window.location - pathname = targetPath || WINDOW.location?.pathname; + const lazyLoadPromise = (async () => { + const result = await originalPatchRoutes(args); + + const currentActiveRootSpan = getActiveRootSpan(); + if (currentActiveRootSpan && (spanToJSON(currentActiveRootSpan) as { op?: string }).op === 'navigation') { + const pathname = isMemoryRouter ? targetPath : targetPath || WINDOW.location?.pathname; + + if (pathname) { + updateNavigationSpan( + currentActiveRootSpan, + { pathname, search: '', hash: '', state: null, key: 'default' }, + Array.from(allRoutes), + false, + _matchRoutes, + ); + } } - if (pathname) { - updateNavigationSpan( - activeRootSpan, - { pathname, search: '', hash: '', state: null, key: 'default' }, - Array.from(allRoutes), - false, // forceUpdate = false since this is after lazy routes are loaded - _matchRoutes, - ); - } + return result; + })(); + + if (activeRootSpan) { + trackLazyRouteLoad(activeRootSpan, lazyLoadPromise); } - return result; + return lazyLoadPromise; }, }; } -function getNavigationKey(location: Location): string { - return `${location.pathname}${location.search}${location.hash}`; -} - -function tryUpdateSpanName( - activeSpan: Span, - currentSpanName: string | undefined, - newName: string, - newSource: string, -): void { - // Check if the new name contains React Router parameter syntax (/:param/) - const isReactRouterParam = /\/:[a-zA-Z0-9_]+/.test(newName); - const isNewNameParameterized = newName !== currentSpanName && isReactRouterParam; - if (isNewNameParameterized) { - activeSpan.updateName(newName); - activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, newSource as 'route' | 'url' | 'custom'); - } -} - -function isDuplicateNavigation(client: Client, navigationKey: string): boolean { - const lastKey = LAST_NAVIGATION_PER_CLIENT.get(client); - return lastKey === navigationKey; -} - -function createNavigationSpan(opts: { - client: Client; - name: string; - source: string; - version: string; - location: Location; - routes: RouteObject[]; - basename?: string; - allRoutes?: RouteObject[]; - navigationKey: string; -}): Span | undefined { - const { client, name, source, version, location, routes, basename, allRoutes, navigationKey } = opts; - - const navigationSpan = startBrowserTracingNavigationSpan(client, { - name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source as 'route' | 'url' | 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter_v${version}`, - }, - }); - - if (navigationSpan) { - LAST_NAVIGATION_PER_CLIENT.set(client, navigationKey); - patchNavigationSpanEnd(navigationSpan, location, routes, basename, allRoutes); - - const unsubscribe = client.on('spanEnd', endedSpan => { - if (endedSpan === navigationSpan) { - // Clear key only if it's still our key (handles overlapping navigations) - const lastKey = LAST_NAVIGATION_PER_CLIENT.get(client); - if (lastKey === navigationKey) { - LAST_NAVIGATION_PER_CLIENT.delete(client); - } - unsubscribe(); // Prevent memory leak - } - }); - } - - return navigationSpan; -} - +// eslint-disable-next-line complexity export function handleNavigation(opts: { location: Location; routes: RouteObject[]; @@ -714,33 +789,84 @@ export function handleNavigation(opts: { basename, ); - const currentNavigationKey = getNavigationKey(location); - const isNavDuplicate = isDuplicateNavigation(client, currentNavigationKey); + const locationKey = computeLocationKey(location); + const trackedNav = activeNavigationSpans.get(client); + + // Determine if this navigation should be skipped as a duplicate + const trackedSpanHasEnded = + trackedNav && !trackedNav.isPlaceholder ? !!spanToJSON(trackedNav.span).timestamp : false; + const { skip, shouldUpdate } = shouldSkipNavigation(trackedNav, locationKey, name, trackedSpanHasEnded); + + if (skip) { + if (shouldUpdate && trackedNav) { + const oldName = trackedNav.routeName; + + if (trackedNav.isPlaceholder) { + // Update placeholder's route name - the real span will be created with this name + trackedNav.routeName = name; + DEBUG_BUILD && + debug.log( + `[Tracing] Updated placeholder navigation name from "${oldName}" to "${name}" (will apply to real span)`, + ); + } else { + // Update existing real span from wildcard to parameterized route name + trackedNav.span.updateName(name); + trackedNav.span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source as 'route' | 'url' | 'custom'); + addNonEnumerableProperty( + trackedNav.span as { __sentry_navigation_name_set__?: boolean }, + '__sentry_navigation_name_set__', + true, + ); + trackedNav.routeName = name; + DEBUG_BUILD && debug.log(`[Tracing] Updated navigation span name from "${oldName}" to "${name}"`); + } + } else { + DEBUG_BUILD && debug.log(`[Tracing] Skipping duplicate navigation for location: ${locationKey}`); + } + return; + } - if (isNavDuplicate) { - // Cross-usage duplicate - update existing span name if better - const activeSpan = getActiveSpan(); - const spanJson = activeSpan && spanToJSON(activeSpan); - const isAlreadyInNavigationSpan = spanJson?.op === 'navigation'; + // Create new navigation span (first navigation or legitimate new navigation) + // Reserve the spot in the map first to prevent race conditions + // Mark as placeholder to prevent concurrent handleNavigation calls from creating duplicates + const placeholderSpan = { end: () => {} } as unknown as Span; + const placeholderEntry = { + span: placeholderSpan, + routeName: name, + pathname: location.pathname, + locationKey, + isPlaceholder: true as const, + }; + activeNavigationSpans.set(client, placeholderEntry); + + let navigationSpan: Span | undefined; + try { + navigationSpan = startBrowserTracingNavigationSpan(client, { + name: placeholderEntry.routeName, // Use placeholder's routeName in case it was updated + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter_v${version}`, + }, + }); + } catch (e) { + // If span creation fails, remove the placeholder so we don't block future navigations + activeNavigationSpans.delete(client); + throw e; + } - if (isAlreadyInNavigationSpan && activeSpan) { - tryUpdateSpanName(activeSpan, spanJson?.description, name, source); - } - } else { - // Not a cross-usage duplicate - create new span - // This handles: different routes, same route with different params (/user/2 → /user/3) - // startBrowserTracingNavigationSpan will end any active navigation span - createNavigationSpan({ - client, - name, - source, - version, - location, - routes, - basename, - allRoutes, - navigationKey: currentNavigationKey, + if (navigationSpan) { + // Update the map with the real span (isPlaceholder omitted, defaults to false) + activeNavigationSpans.set(client, { + span: navigationSpan, + routeName: placeholderEntry.routeName, // Use the (potentially updated) placeholder routeName + pathname: location.pathname, + locationKey, }); + patchSpanEnd(navigationSpan, location, routes, basename, allRoutes, 'navigation'); + } else { + // If no span was created, remove the placeholder + activeNavigationSpans.delete(client); } } } @@ -809,11 +935,93 @@ function updatePageloadTransaction({ activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); // Patch span.end() to ensure we update the name one last time before the span is sent - patchPageloadSpanEnd(activeRootSpan, location, routes, basename, allRoutes); + patchSpanEnd(activeRootSpan, location, routes, basename, allRoutes, 'pageload'); } } } +/** + * Determines if a span name should be updated during wildcard route resolution. + * + * Update conditions (in priority order): + * 1. No current name + allowNoCurrentName: true → always update (pageload spans) + * 2. Current name has wildcard + new is route without wildcard → upgrade (e.g., "/users/*" → "/users/:id") + * 3. Current source is not 'route' + new source is 'route' → upgrade (e.g., URL → parameterized route) + * + * @param currentName - The current span name (may be undefined) + * @param currentSource - The current span source ('route', 'url', or undefined) + * @param newName - The proposed new span name + * @param newSource - The proposed new span source + * @param allowNoCurrentName - If true, allow updates when there's no current name (for pageload spans) + * @returns true if the span name should be updated + */ +function shouldUpdateWildcardSpanName( + currentName: string | undefined, + currentSource: string | undefined, + newName: string, + newSource: string, + allowNoCurrentName = false, +): boolean { + if (!newName) { + return false; + } + + if (!currentName && allowNoCurrentName) { + return true; + } + + const hasWildcard = currentName && transactionNameHasWildcard(currentName); + + if (hasWildcard && newSource === 'route' && !transactionNameHasWildcard(newName)) { + return true; + } + + if (currentSource !== 'route' && newSource === 'route') { + return true; + } + + return false; +} + +function tryUpdateSpanNameBeforeEnd( + span: Span, + spanJson: ReturnType, + currentName: string | undefined, + location: Location, + routes: RouteObject[], + basename: string | undefined, + spanType: 'pageload' | 'navigation', + allRoutes: Set, +): void { + try { + const currentSource = spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + + if (currentSource === 'route' && currentName && !transactionNameHasWildcard(currentName)) { + return; + } + + const currentAllRoutes = Array.from(allRoutes); + const routesToUse = currentAllRoutes.length > 0 ? currentAllRoutes : routes; + const branches = _matchRoutes(routesToUse, location, basename) as unknown as RouteMatch[]; + + if (!branches) { + return; + } + + const [name, source] = resolveRouteNameAndSource(location, routesToUse, routesToUse, branches, basename); + + const isImprovement = shouldUpdateWildcardSpanName(currentName, currentSource, name, source, true); + const spanNotEnded = spanType === 'pageload' || !spanJson.timestamp; + + if (isImprovement && spanNotEnded) { + span.updateName(name); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + } + } catch (error) { + DEBUG_BUILD && debug.warn(`Error updating span details before ending: ${error}`); + } +} + /** * Patches the span.end() method to update the transaction name one last time before the span is sent. * This handles cases where the span is cancelled early (e.g., document.hidden) before lazy routes have finished loading. @@ -833,71 +1041,93 @@ function patchSpanEnd( return; } + // Use the passed route context, or fall back to global Set + const allRoutesSet = _allRoutes ? new Set(_allRoutes) : allRoutes; + const originalEnd = span.end.bind(span); + let endCalled = false; span.end = function patchedEnd(...args) { - try { - // Only update if the span source is not already 'route' (i.e., it hasn't been parameterized yet) - const spanJson = spanToJSON(span); - const currentSource = spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; - if (currentSource !== 'route') { - // Last chance to update the transaction name with the latest route info - // Use the live global allRoutes Set to include any lazy routes loaded after patching - const currentAllRoutes = Array.from(allRoutes); - const branches = _matchRoutes( - currentAllRoutes.length > 0 ? currentAllRoutes : routes, - location, - basename, - ) as unknown as RouteMatch[]; + if (endCalled) { + return; + } + endCalled = true; + + // Capture timestamp immediately to avoid delay from async operations + // If no timestamp was provided, capture the current time now + const endTimestamp = args.length > 0 ? args[0] : Date.now() / 1000; + + const spanJson = spanToJSON(span); + const currentName = spanJson.description; + const currentSource = spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + + // Helper to clean up activeNavigationSpans after span ends + const cleanupNavigationSpan = (): void => { + const client = getClient(); + if (client && spanType === 'navigation') { + const trackedNav = activeNavigationSpans.get(client); + if (trackedNav && trackedNav.span === span) { + activeNavigationSpans.delete(client); + } + } + }; + + const pendingPromises = pendingLazyRouteLoads.get(span); + // Wait for lazy routes if: + // 1. There are pending promises AND + // 2. Current name exists AND + // 3. Either the name has a wildcard OR the source is not 'route' (URL-based names) + const shouldWaitForLazyRoutes = + pendingPromises && + pendingPromises.size > 0 && + currentName && + (transactionNameHasWildcard(currentName) || currentSource !== 'route'); + + if (shouldWaitForLazyRoutes) { + if (_lazyRouteTimeout === 0) { + tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType, allRoutesSet); + cleanupNavigationSpan(); + originalEnd(endTimestamp); + return; + } - if (branches) { - const [name, source] = resolveRouteNameAndSource( + const allSettled = Promise.allSettled(pendingPromises).then(() => {}); + const waitPromise = + _lazyRouteTimeout === Infinity + ? allSettled + : Promise.race([allSettled, new Promise(r => setTimeout(r, _lazyRouteTimeout))]); + + waitPromise + .then(() => { + const updatedSpanJson = spanToJSON(span); + tryUpdateSpanNameBeforeEnd( + span, + updatedSpanJson, + updatedSpanJson.description, location, - currentAllRoutes.length > 0 ? currentAllRoutes : routes, - currentAllRoutes.length > 0 ? currentAllRoutes : routes, - branches, + routes, basename, + spanType, + allRoutesSet, ); - - // Only update if we have a valid name - if (name && (spanType === 'pageload' || !spanJson.timestamp)) { - span.updateName(name); - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); - } - } - } - } catch (error) { - // Silently catch errors to ensure span.end() is always called - DEBUG_BUILD && debug.warn(`Error updating span details before ending: ${error}`); + cleanupNavigationSpan(); + originalEnd(endTimestamp); + }) + .catch(() => { + cleanupNavigationSpan(); + originalEnd(endTimestamp); + }); + return; } - return originalEnd(...args); + tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType, allRoutesSet); + cleanupNavigationSpan(); + originalEnd(endTimestamp); }; - // Mark this span as having its end() method patched to prevent duplicate patching addNonEnumerableProperty(span as unknown as Record, patchedPropertyName, true); } -function patchPageloadSpanEnd( - span: Span, - location: Location, - routes: RouteObject[], - basename: string | undefined, - _allRoutes: RouteObject[] | undefined, -): void { - patchSpanEnd(span, location, routes, basename, _allRoutes, 'pageload'); -} - -function patchNavigationSpanEnd( - span: Span, - location: Location, - routes: RouteObject[], - basename: string | undefined, - _allRoutes: RouteObject[] | undefined, -): void { - patchSpanEnd(span, location, routes, basename, _allRoutes, 'navigation'); -} - // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createV6CompatibleWithSentryReactRouterRouting

, R extends React.FC

>( Routes: R, @@ -933,11 +1163,13 @@ export function createV6CompatibleWithSentryReactRouterRouting

{ return { ...(actual as any), startBrowserTracingNavigationSpan: vi.fn(), + startBrowserTracingPageLoadSpan: vi.fn(), browserTracingIntegration: vi.fn(() => ({ setup: vi.fn(), afterAllSetup: vi.fn(), @@ -49,6 +56,9 @@ vi.mock('../../src/reactrouter-compat-utils/utils', () => ({ getGlobalLocation: vi.fn(() => ({ pathname: '/test', search: '', hash: '' })), getGlobalPathname: vi.fn(() => '/test'), routeIsDescendant: vi.fn(() => false), + transactionNameHasWildcard: vi.fn((name: string) => { + return name.includes('/*') || name === '*' || name.endsWith('*'); + }), })); vi.mock('../../src/reactrouter-compat-utils/lazy-routes', () => ({ @@ -370,3 +380,932 @@ describe('addRoutesToAllRoutes', () => { expect(firstCount).toBe(secondCount); }); }); + +describe('updateNavigationSpan with wildcard detection', () => { + const sampleLocation: Location = { + pathname: '/test', + search: '', + hash: '', + state: null, + key: 'default', + }; + + const sampleRoutes: RouteObject[] = [ + { path: '/', element:

Home
}, + { path: '/about', element:
About
}, + ]; + + const mockMatchRoutes = vi.fn(() => []); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should call updateName when provided with valid routes', () => { + const testSpan = { ...mockSpan }; + updateNavigationSpan(testSpan, sampleLocation, sampleRoutes, false, mockMatchRoutes); + + expect(mockUpdateName).toHaveBeenCalledWith('Test Route'); + expect(mockSetAttribute).toHaveBeenCalledWith('sentry.source', 'route'); + }); + + it('should handle forced updates', () => { + const testSpan = { ...mockSpan, __sentry_navigation_name_set__: true }; + updateNavigationSpan(testSpan, sampleLocation, sampleRoutes, true, mockMatchRoutes); + + // Should update even though already named because forceUpdate=true + expect(mockUpdateName).toHaveBeenCalledWith('Test Route'); + }); +}); + +describe('tryUpdateSpanNameBeforeEnd - source upgrade logic', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should upgrade from URL source to route source (regression fix)', async () => { + // Setup: Current span has URL source and non-parameterized name + vi.mocked(spanToJSON).mockReturnValue({ + op: 'navigation', + description: '/users/123', + data: { 'sentry.source': 'url' }, + } as any); + + // Target: Resolves to route source with parameterized name + vi.mocked(resolveRouteNameAndSource).mockReturnValue(['/users/:id', 'route']); + + const mockUpdateName = vi.fn(); + const mockSetAttribute = vi.fn(); + const testSpan = { + updateName: mockUpdateName, + setAttribute: mockSetAttribute, + end: vi.fn(), + } as unknown as Span; + + // Simulate patchSpanEnd calling tryUpdateSpanNameBeforeEnd + // by updating the span name during a navigation + updateNavigationSpan( + testSpan, + { pathname: '/users/123', search: '', hash: '', state: null, key: 'test' }, + [{ path: '/users/:id', element:
}], + false, + vi.fn(() => [{ route: { path: '/users/:id' } }]), + ); + + // Should upgrade from URL to route source + expect(mockUpdateName).toHaveBeenCalledWith('/users/:id'); + expect(mockSetAttribute).toHaveBeenCalledWith('sentry.source', 'route'); + }); + + it('should not downgrade from route source to URL source', async () => { + // Setup: Current span has route source with parameterized name (no wildcard) + vi.mocked(spanToJSON).mockReturnValue({ + op: 'navigation', + description: '/users/:id', + data: { 'sentry.source': 'route' }, + } as any); + + // Target: Would resolve to URL source (downgrade attempt) + vi.mocked(resolveRouteNameAndSource).mockReturnValue(['/users/456', 'url']); + + const mockUpdateName = vi.fn(); + const mockSetAttribute = vi.fn(); + const testSpan = { + updateName: mockUpdateName, + setAttribute: mockSetAttribute, + end: vi.fn(), + __sentry_navigation_name_set__: true, // Mark as already named + } as unknown as Span; + + updateNavigationSpan( + testSpan, + { pathname: '/users/456', search: '', hash: '', state: null, key: 'test' }, + [{ path: '/users/:id', element:
}], + false, + vi.fn(() => [{ route: { path: '/users/:id' } }]), + ); + + // Should not update because span is already named + // The early return in tryUpdateSpanNameBeforeEnd protects against downgrades + // This test verifies that route->url downgrades are blocked + expect(mockUpdateName).not.toHaveBeenCalled(); + expect(mockSetAttribute).not.toHaveBeenCalled(); + }); + + it('should upgrade wildcard names to specific routes', async () => { + // Setup: Current span has route source with wildcard + vi.mocked(spanToJSON).mockReturnValue({ + op: 'navigation', + description: '/users/*', + data: { 'sentry.source': 'route' }, + } as any); + + // Mock wildcard detection: current name has wildcard, new name doesn't + vi.mocked(transactionNameHasWildcard).mockImplementation((name: string) => { + return name === '/users/*'; // Only the current name has wildcard + }); + + // Target: Resolves to specific parameterized route + vi.mocked(resolveRouteNameAndSource).mockReturnValue(['/users/:id', 'route']); + + const mockUpdateName = vi.fn(); + const mockSetAttribute = vi.fn(); + const testSpan = { + updateName: mockUpdateName, + setAttribute: mockSetAttribute, + end: vi.fn(), + } as unknown as Span; + + updateNavigationSpan( + testSpan, + { pathname: '/users/123', search: '', hash: '', state: null, key: 'test' }, + [{ path: '/users/:id', element:
}], + false, + vi.fn(() => [{ route: { path: '/users/:id' } }]), + ); + + // Should upgrade from wildcard to specific + expect(mockUpdateName).toHaveBeenCalledWith('/users/:id'); + expect(mockSetAttribute).toHaveBeenCalledWith('sentry.source', 'route'); + }); + + it('should not downgrade from wildcard route to URL', async () => { + // Setup: Current span has route source with wildcard + vi.mocked(spanToJSON).mockReturnValue({ + op: 'navigation', + description: '/users/*', + data: { 'sentry.source': 'route' }, + } as any); + + // Mock wildcard detection: current name has wildcard, new name doesn't + vi.mocked(transactionNameHasWildcard).mockImplementation((name: string) => { + return name === '/users/*'; // Only the current wildcard name returns true + }); + + // Target: After timeout, resolves to URL (lazy route didn't finish loading) + vi.mocked(resolveRouteNameAndSource).mockReturnValue(['/users/123', 'url']); + + const mockUpdateName = vi.fn(); + const mockSetAttribute = vi.fn(); + const testSpan = { + updateName: mockUpdateName, + setAttribute: mockSetAttribute, + end: vi.fn(), + __sentry_navigation_name_set__: true, // Mark span as already named/finalized + } as unknown as Span; + + updateNavigationSpan( + testSpan, + { pathname: '/users/123', search: '', hash: '', state: null, key: 'test' }, + [{ path: '/users/*', element:
}], + false, + vi.fn(() => [{ route: { path: '/users/*' } }]), + ); + + // Should not update - keep wildcard route instead of downgrading to URL + // Wildcard routes are better than URLs for aggregation in performance monitoring + expect(mockUpdateName).not.toHaveBeenCalled(); + expect(mockSetAttribute).not.toHaveBeenCalled(); + }); + + it('should set name when no current name exists', async () => { + // Setup: Current span has no name (undefined) + vi.mocked(spanToJSON).mockReturnValue({ + op: 'navigation', + description: undefined, + } as any); + + // Target: Resolves to route + vi.mocked(resolveRouteNameAndSource).mockReturnValue(['/users/:id', 'route']); + + const mockUpdateName = vi.fn(); + const mockSetAttribute = vi.fn(); + const testSpan = { + updateName: mockUpdateName, + setAttribute: mockSetAttribute, + end: vi.fn(), + } as unknown as Span; + + updateNavigationSpan( + testSpan, + { pathname: '/users/123', search: '', hash: '', state: null, key: 'test' }, + [{ path: '/users/:id', element:
}], + false, + vi.fn(() => [{ route: { path: '/users/:id' } }]), + ); + + // Should set initial name + expect(mockUpdateName).toHaveBeenCalledWith('/users/:id'); + expect(mockSetAttribute).toHaveBeenCalledWith('sentry.source', 'route'); + }); + + it('should not update when same source and no improvement', async () => { + // Setup: Current span has URL source + vi.mocked(spanToJSON).mockReturnValue({ + op: 'navigation', + description: '/users/123', + data: { 'sentry.source': 'url' }, + } as any); + + // Target: Resolves to same URL source (no improvement) + vi.mocked(resolveRouteNameAndSource).mockReturnValue(['/users/123', 'url']); + + const mockUpdateName = vi.fn(); + const mockSetAttribute = vi.fn(); + const testSpan = { + updateName: mockUpdateName, + setAttribute: mockSetAttribute, + end: vi.fn(), + } as unknown as Span; + + updateNavigationSpan( + testSpan, + { pathname: '/users/123', search: '', hash: '', state: null, key: 'test' }, + [{ path: '/users/:id', element:
}], + false, + vi.fn(() => [{ route: { path: '/users/:id' } }]), + ); + + // Note: updateNavigationSpan always updates if not already named + // This test validates that the isImprovement logic works correctly in tryUpdateSpanNameBeforeEnd + // which is called during span.end() patching + expect(mockUpdateName).toHaveBeenCalled(); // Initial set is allowed + }); + + describe('computeLocationKey (pure function)', () => { + it('should include pathname, search, and hash in location key', () => { + const location: Location = { + pathname: '/search', + search: '?q=foo', + hash: '#results', + state: null, + key: 'test', + }; + + const result = computeLocationKey(location); + + expect(result).toBe('/search?q=foo#results'); + }); + + it('should differentiate locations with same pathname but different query', () => { + const loc1: Location = { pathname: '/search', search: '?q=foo', hash: '', state: null, key: 'k1' }; + const loc2: Location = { pathname: '/search', search: '?q=bar', hash: '', state: null, key: 'k2' }; + + const key1 = computeLocationKey(loc1); + const key2 = computeLocationKey(loc2); + + // Verifies that search params are included in the location key + expect(key1).not.toBe(key2); + expect(key1).toBe('/search?q=foo'); + expect(key2).toBe('/search?q=bar'); + }); + + it('should differentiate locations with same pathname but different hash', () => { + const loc1: Location = { pathname: '/page', search: '', hash: '#section1', state: null, key: 'k1' }; + const loc2: Location = { pathname: '/page', search: '', hash: '#section2', state: null, key: 'k2' }; + + const key1 = computeLocationKey(loc1); + const key2 = computeLocationKey(loc2); + + // Verifies that hash values are included in the location key + expect(key1).not.toBe(key2); + expect(key1).toBe('/page#section1'); + expect(key2).toBe('/page#section2'); + }); + + it('should produce same key for identical locations', () => { + const loc1: Location = { pathname: '/users', search: '?id=123', hash: '#profile', state: null, key: 'k1' }; + const loc2: Location = { pathname: '/users', search: '?id=123', hash: '#profile', state: null, key: 'k2' }; + + expect(computeLocationKey(loc1)).toBe(computeLocationKey(loc2)); + }); + + it('should normalize undefined/null search and hash to empty strings (partial location objects)', () => { + // When receives a string, React Router creates a partial location + // with search: undefined and hash: undefined. We must normalize these to empty strings + // to match the keys from full location objects (which have search: '' and hash: ''). + // This prevents duplicate navigation spans when using prop (common in modal routes). + const partialLocation: Location = { + pathname: '/users', + search: undefined as unknown as string, + hash: undefined as unknown as string, + state: null, + key: 'test1', + }; + + const fullLocation: Location = { + pathname: '/users', + search: '', + hash: '', + state: null, + key: 'test2', + }; + + const partialKey = computeLocationKey(partialLocation); + const fullKey = computeLocationKey(fullLocation); + + // Verifies that undefined values are normalized to empty strings, preventing + // '/usersundefinedundefined' !== '/users' mismatches + expect(partialKey).toBe('/users'); + expect(fullKey).toBe('/users'); + expect(partialKey).toBe(fullKey); + }); + + it('should normalize null search and hash to empty strings', () => { + const locationWithNulls: Location = { + pathname: '/products', + search: null as unknown as string, + hash: null as unknown as string, + state: null, + key: 'test3', + }; + + const locationWithEmptyStrings: Location = { + pathname: '/products', + search: '', + hash: '', + state: null, + key: 'test4', + }; + + expect(computeLocationKey(locationWithNulls)).toBe('/products'); + expect(computeLocationKey(locationWithEmptyStrings)).toBe('/products'); + expect(computeLocationKey(locationWithNulls)).toBe(computeLocationKey(locationWithEmptyStrings)); + }); + }); + + describe('shouldSkipNavigation (pure function - duplicate detection logic)', () => { + const mockSpan: Span = { updateName: vi.fn(), setAttribute: vi.fn(), end: vi.fn() } as unknown as Span; + + it('should not skip when no tracked navigation exists', () => { + const result = shouldSkipNavigation(undefined, '/users', '/users/:id', false); + + expect(result).toEqual({ skip: false, shouldUpdate: false }); + }); + + it('should skip placeholder navigations for same locationKey', () => { + const trackedNav = { + span: mockSpan, + routeName: '/search', + pathname: '/search', + locationKey: '/search?q=foo', + isPlaceholder: true, + }; + + const result = shouldSkipNavigation(trackedNav, '/search?q=foo', '/search', false); + + // Verifies that placeholder navigations for the same locationKey are skipped + expect(result.skip).toBe(true); + expect(result.shouldUpdate).toBe(false); + }); + + it('should NOT skip placeholder navigations for different locationKey (query change)', () => { + const trackedNav = { + span: mockSpan, + routeName: '/search', + pathname: '/search', + locationKey: '/search?q=foo', + isPlaceholder: true, + }; + + const result = shouldSkipNavigation(trackedNav, '/search?q=bar', '/search', false); + + // Verifies that different locationKeys allow new navigation even with same pathname + expect(result.skip).toBe(false); + expect(result.shouldUpdate).toBe(false); + }); + + it('should skip real span navigations for same locationKey when span has not ended', () => { + const trackedNav = { + span: mockSpan, + routeName: '/users/:id', + pathname: '/users/123', + locationKey: '/users/123?tab=profile', + isPlaceholder: false, + }; + + const result = shouldSkipNavigation(trackedNav, '/users/123?tab=profile', '/users/:id', false); + + // Verifies that duplicate navigations are blocked when span hasn't ended + expect(result.skip).toBe(true); + }); + + it('should NOT skip real span navigations for different locationKey (query change)', () => { + const trackedNav = { + span: mockSpan, + routeName: '/users/:id', + pathname: '/users/123', + locationKey: '/users/123?tab=profile', + isPlaceholder: false, + }; + + const result = shouldSkipNavigation(trackedNav, '/users/123?tab=settings', '/users/:id', false); + + // Verifies that different locationKeys allow new navigation even with same pathname + expect(result.skip).toBe(false); + }); + + it('should NOT skip when tracked span has ended', () => { + const trackedNav = { + span: mockSpan, + routeName: '/users/:id', + pathname: '/users/123', + locationKey: '/users/123', + isPlaceholder: false, + }; + + const result = shouldSkipNavigation(trackedNav, '/users/123', '/users/:id', true); + + // Allow new navigation when previous span has ended + expect(result.skip).toBe(false); + }); + + it('should set shouldUpdate=true for wildcard to parameterized upgrade', () => { + const trackedNav = { + span: mockSpan, + routeName: '/users/*', + pathname: '/users/123', + locationKey: '/users/123', + isPlaceholder: false, + }; + + const result = shouldSkipNavigation(trackedNav, '/users/123', '/users/:id', false); + + // Verifies that wildcard names are upgraded to parameterized routes + expect(result.skip).toBe(true); + expect(result.shouldUpdate).toBe(true); + }); + + it('should NOT set shouldUpdate=true when both names are wildcards', () => { + const trackedNav = { + span: mockSpan, + routeName: '/users/*', + pathname: '/users/123', + locationKey: '/users/123', + isPlaceholder: false, + }; + + const result = shouldSkipNavigation(trackedNav, '/users/123', '/users/*', false); + + expect(result.skip).toBe(true); + expect(result.shouldUpdate).toBe(false); + }); + }); + + describe('handleNavigation integration (verifies wiring to pure functions)', () => { + // Verifies that handleNavigation correctly uses computeLocationKey and shouldSkipNavigation + + let mockNavigationSpan: Span; + + beforeEach(async () => { + // Reset all mocks + vi.clearAllMocks(); + + // Import fresh modules to reset internal state + const coreModule = await import('@sentry/core'); + const browserModule = await import('@sentry/browser'); + const instrumentationModule = await import('../../src/reactrouter-compat-utils/instrumentation'); + + // Create a mock span with end() that captures callback + mockNavigationSpan = { + updateName: vi.fn(), + setAttribute: vi.fn(), + end: vi.fn(), + } as unknown as Span; + + // Mock getClient to return a client that's registered for instrumentation + const mockClient = { + addIntegration: vi.fn(), + emit: vi.fn(), + on: vi.fn(), + getOptions: vi.fn(() => ({})), + } as unknown as Client; + vi.mocked(coreModule.getClient).mockReturnValue(mockClient); + + // Mock startBrowserTracingPageLoadSpan to avoid pageload span creation during setup + vi.mocked(browserModule.startBrowserTracingPageLoadSpan).mockReturnValue(undefined); + + // Register client for instrumentation by adding it to the internal set + const integration = instrumentationModule.createReactRouterV6CompatibleTracingIntegration({ + useEffect: vi.fn(), + useLocation: vi.fn(), + useNavigationType: vi.fn(), + createRoutesFromChildren: vi.fn(), + matchRoutes: vi.fn(), + }); + integration.afterAllSetup(mockClient); + + // Mock startBrowserTracingNavigationSpan to return our mock span + vi.mocked(browserModule.startBrowserTracingNavigationSpan).mockReturnValue(mockNavigationSpan); + + // Mock spanToJSON to return different values for different calls + vi.mocked(coreModule.spanToJSON).mockReturnValue({ op: 'navigation' } as any); + + // Mock getActiveRootSpan to return undefined (no pageload span) + vi.mocked(coreModule.getActiveSpan).mockReturnValue(undefined); + }); + + it('creates navigation span and uses computeLocationKey for tracking', async () => { + const { handleNavigation } = await import('../../src/reactrouter-compat-utils/instrumentation'); + const { startBrowserTracingNavigationSpan } = await import('@sentry/browser'); + const { resolveRouteNameAndSource } = await import('../../src/reactrouter-compat-utils/utils'); + + // Mock to return a specific route name + vi.mocked(resolveRouteNameAndSource).mockReturnValue(['/search', 'route']); + + const location: Location = { + pathname: '/search', + search: '?q=foo', + hash: '#results', + state: null, + key: 'test1', + }; + + const matches = [ + { + pathname: '/search', + pathnameBase: '/search', + route: { path: '/search', element:
}, + params: {}, + }, + ]; + + handleNavigation({ + location, + routes: [{ path: '/search', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + // Verifies that handleNavigation calls startBrowserTracingNavigationSpan + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledWith( + expect.objectContaining({ emit: expect.any(Function) }), // client + expect.objectContaining({ + name: '/search', + attributes: expect.objectContaining({ + 'sentry.op': 'navigation', + 'sentry.source': 'route', + }), + }), + ); + }); + + it('blocks duplicate navigation for exact same locationKey (pathname+query+hash)', async () => { + const { handleNavigation } = await import('../../src/reactrouter-compat-utils/instrumentation'); + const { startBrowserTracingNavigationSpan } = await import('@sentry/browser'); + const { spanToJSON } = await import('@sentry/core'); + + const location: Location = { + pathname: '/search', + search: '?q=foo', + hash: '#results', + state: null, + key: 'test1', + }; + + const matches = [ + { + pathname: '/search', + pathnameBase: '/search', + route: { path: '/search', element:
}, + params: {}, + }, + ]; + + // First navigation - should create span + handleNavigation({ + location, + routes: [{ path: '/search', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + // Mock spanToJSON to indicate span hasn't ended yet + vi.mocked(spanToJSON).mockReturnValue({ op: 'navigation' } as any); + + // Second navigation - exact same location, should be blocked + handleNavigation({ + location: { ...location, key: 'test2' }, // Different key, same location + routes: [{ path: '/search', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + // Verifies that duplicate detection uses locationKey (not just pathname) + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); // Only first call + }); + + it('allows navigation for same pathname but different query string', async () => { + const { handleNavigation } = await import('../../src/reactrouter-compat-utils/instrumentation'); + const { startBrowserTracingNavigationSpan } = await import('@sentry/browser'); + const { spanToJSON } = await import('@sentry/core'); + + const location1: Location = { + pathname: '/search', + search: '?q=foo', + hash: '', + state: null, + key: 'test1', + }; + + const matches = [ + { + pathname: '/search', + pathnameBase: '/search', + route: { path: '/search', element:
}, + params: {}, + }, + ]; + + // First navigation + handleNavigation({ + location: location1, + routes: [{ path: '/search', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + // Mock spanToJSON to indicate span hasn't ended yet + vi.mocked(spanToJSON).mockReturnValue({ op: 'navigation' } as any); + + // Second navigation - same pathname, different query + const location2: Location = { + pathname: '/search', + search: '?q=bar', + hash: '', + state: null, + key: 'test2', + }; + + handleNavigation({ + location: location2, + routes: [{ path: '/search', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + // Verifies that query params are included in locationKey for duplicate detection + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); // Both calls should create spans + }); + + it('allows navigation for same pathname but different hash', async () => { + const { handleNavigation } = await import('../../src/reactrouter-compat-utils/instrumentation'); + const { startBrowserTracingNavigationSpan } = await import('@sentry/browser'); + const { spanToJSON } = await import('@sentry/core'); + + const location1: Location = { + pathname: '/page', + search: '', + hash: '#section1', + state: null, + key: 'test1', + }; + + const matches = [ + { + pathname: '/page', + pathnameBase: '/page', + route: { path: '/page', element:
}, + params: {}, + }, + ]; + + // First navigation + handleNavigation({ + location: location1, + routes: [{ path: '/page', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + // Mock spanToJSON to indicate span hasn't ended yet + vi.mocked(spanToJSON).mockReturnValue({ op: 'navigation' } as any); + + // Second navigation - same pathname, different hash + const location2: Location = { + pathname: '/page', + search: '', + hash: '#section2', + state: null, + key: 'test2', + }; + + handleNavigation({ + location: location2, + routes: [{ path: '/page', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + // Verifies that hash values are included in locationKey for duplicate detection + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); // Both calls should create spans + }); + + it('updates wildcard span when better parameterized name becomes available', async () => { + const { handleNavigation } = await import('../../src/reactrouter-compat-utils/instrumentation'); + const { startBrowserTracingNavigationSpan } = await import('@sentry/browser'); + const { spanToJSON } = await import('@sentry/core'); + const { transactionNameHasWildcard, resolveRouteNameAndSource } = await import( + '../../src/reactrouter-compat-utils/utils' + ); + + const location: Location = { + pathname: '/users/123', + search: '', + hash: '', + state: null, + key: 'test1', + }; + + const matches = [ + { + pathname: '/users/123', + pathnameBase: '/users', + route: { path: '/users/*', element:
}, + params: { '*': '123' }, + }, + ]; + + // First navigation - resolves to wildcard name + vi.mocked(resolveRouteNameAndSource).mockReturnValue(['/users/*', 'route']); + // Mock transactionNameHasWildcard to return true for wildcards, false for parameterized + vi.mocked(transactionNameHasWildcard).mockImplementation((name: string) => { + return name.includes('/*') || name === '*' || name.endsWith('*'); + }); + + handleNavigation({ + location, + routes: [{ path: '/users/*', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + const firstSpan = mockNavigationSpan; + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + + // Mock spanToJSON to indicate span hasn't ended yet and has wildcard name + vi.mocked(spanToJSON).mockReturnValue({ + op: 'navigation', + description: '/users/*', + data: { 'sentry.source': 'route' }, + } as any); + + // Second navigation - same location but better parameterized name available + vi.mocked(resolveRouteNameAndSource).mockReturnValue(['/users/:id', 'route']); + + handleNavigation({ + location: { ...location, key: 'test2' }, + routes: [{ path: '/users/:id', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + // Verifies that wildcard span names are upgraded when parameterized routes become available + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(vi.mocked(firstSpan.updateName)).toHaveBeenCalledWith('/users/:id'); + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); // No new span created + }); + + it('prevents duplicate spans when prop is a string (partial location)', async () => { + // This test verifies the fix for the bug where creates + // a partial location object with search: undefined and hash: undefined, which + // would result in a different locationKey ('/usersundefinedundefined' vs '/users') + // causing duplicate navigation spans. + const { handleNavigation } = await import('../../src/reactrouter-compat-utils/instrumentation'); + const { startBrowserTracingNavigationSpan } = await import('@sentry/browser'); + const { spanToJSON } = await import('@sentry/core'); + const { resolveRouteNameAndSource } = await import('../../src/reactrouter-compat-utils/utils'); + + // Mock resolveRouteNameAndSource to return consistent route name + vi.mocked(resolveRouteNameAndSource).mockReturnValue(['/users', 'route']); + + const matches = [ + { + pathname: '/users', + pathnameBase: '/users', + route: { path: '/users', element:
}, + params: {}, + }, + ]; + + // First call: Partial location (from ) + // React Router creates location with undefined search and hash + const partialLocation: Location = { + pathname: '/users', + search: undefined as unknown as string, + hash: undefined as unknown as string, + state: null, + key: 'test1', + }; + + handleNavigation({ + location: partialLocation, + routes: [{ path: '/users', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + + // Mock spanToJSON to indicate span hasn't ended yet + vi.mocked(spanToJSON).mockReturnValue({ op: 'navigation' } as any); + + // Second call: Full location (from router.state) + // React Router provides location with empty string search and hash + const fullLocation: Location = { + pathname: '/users', + search: '', + hash: '', + state: null, + key: 'test2', + }; + + handleNavigation({ + location: fullLocation, + routes: [{ path: '/users', element:
}], + navigationType: 'PUSH', + version: '6' as const, + matches: matches as any, + }); + + // Verifies that undefined values are normalized, preventing duplicate spans + // (without normalization, '/usersundefinedundefined' != '/users' would create 2 spans) + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + }); + }); + + describe('SSR-safe RAF fallback (scheduleCallback/cancelScheduledCallback)', () => { + // These tests verify that the RAF fallback works correctly in SSR environments + + it('uses requestAnimationFrame when available', () => { + // Save original RAF + const originalRAF = window.requestAnimationFrame; + const rafSpy = vi.fn((cb: () => void) => { + cb(); + return 123; + }); + window.requestAnimationFrame = rafSpy; + + try { + // Import module to trigger RAF usage + const scheduleCallback = (callback: () => void): number => { + if (window?.requestAnimationFrame) { + return window.requestAnimationFrame(callback); + } + return setTimeout(callback, 0) as unknown as number; + }; + + const mockCallback = vi.fn(); + scheduleCallback(mockCallback); + + // Verifies that requestAnimationFrame is used when available + expect(rafSpy).toHaveBeenCalled(); + expect(mockCallback).toHaveBeenCalled(); + } finally { + window.requestAnimationFrame = originalRAF; + } + }); + + it('falls back to setTimeout when requestAnimationFrame is unavailable (SSR)', () => { + // Simulate SSR by removing RAF + const originalRAF = window.requestAnimationFrame; + const originalCAF = window.cancelAnimationFrame; + // @ts-expect-error - Simulating SSR environment + delete window.requestAnimationFrame; + // @ts-expect-error - Simulating SSR environment + delete window.cancelAnimationFrame; + + try { + const timeoutSpy = vi.spyOn(global, 'setTimeout'); + + // Import module to trigger setTimeout fallback + const scheduleCallback = (callback: () => void): number => { + if (window?.requestAnimationFrame) { + return window.requestAnimationFrame(callback); + } + return setTimeout(callback, 0) as unknown as number; + }; + + const mockCallback = vi.fn(); + scheduleCallback(mockCallback); + + // Verifies that setTimeout is used when requestAnimationFrame is unavailable + expect(timeoutSpy).toHaveBeenCalledWith(mockCallback, 0); + } finally { + window.requestAnimationFrame = originalRAF; + window.cancelAnimationFrame = originalCAF; + } + }); + }); +}); diff --git a/packages/react/test/reactrouter-compat-utils/utils.test.ts b/packages/react/test/reactrouter-compat-utils/utils.test.ts index 9ff48e7450bc..438b026104bd 100644 --- a/packages/react/test/reactrouter-compat-utils/utils.test.ts +++ b/packages/react/test/reactrouter-compat-utils/utils.test.ts @@ -9,6 +9,7 @@ import { prefixWithSlash, rebuildRoutePathFromAllRoutes, resolveRouteNameAndSource, + transactionNameHasWildcard, } from '../../src/reactrouter-compat-utils'; import type { Location, MatchRoutes, RouteMatch, RouteObject } from '../../src/types'; @@ -629,4 +630,38 @@ describe('reactrouter-compat-utils/utils', () => { expect(result).toEqual(['/unknown', 'url']); }); }); + + describe('transactionNameHasWildcard', () => { + it('should detect wildcard at the end of path', () => { + expect(transactionNameHasWildcard('/lazy/*')).toBe(true); + expect(transactionNameHasWildcard('/users/:id/*')).toBe(true); + expect(transactionNameHasWildcard('/products/:category/*')).toBe(true); + }); + + it('should detect standalone wildcard', () => { + expect(transactionNameHasWildcard('*')).toBe(true); + }); + + it('should detect wildcard in the middle of path', () => { + expect(transactionNameHasWildcard('/lazy/*/nested')).toBe(true); + expect(transactionNameHasWildcard('/a/*/b/*/c')).toBe(true); + }); + + it('should not detect wildcards in parameterized routes', () => { + expect(transactionNameHasWildcard('/users/:id')).toBe(false); + expect(transactionNameHasWildcard('/products/:category/:id')).toBe(false); + expect(transactionNameHasWildcard('/items/:itemId/details')).toBe(false); + }); + + it('should not detect wildcards in static routes', () => { + expect(transactionNameHasWildcard('/')).toBe(false); + expect(transactionNameHasWildcard('/about')).toBe(false); + expect(transactionNameHasWildcard('/users/profile')).toBe(false); + }); + + it('should handle edge cases', () => { + expect(transactionNameHasWildcard('')).toBe(false); + expect(transactionNameHasWildcard('/path/to/asterisk')).toBe(false); // 'asterisk' contains 'isk' but not '*' + }); + }); }); From 2ee464fc1a00c60e9438e6a5c27dc09e976e5e9c Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Mon, 24 Nov 2025 10:14:07 +0100 Subject: [PATCH 24/32] chore: Add external contributor to CHANGELOG.md (#18297) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #18281 Co-authored-by: nicohrubec <29484629+nicohrubec@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 479b72fc2f08..ac6b755cbb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @bignoncedric. Thank you for your contribution! + - feat(deps): Bump OpenTelemetry ([#18239](https://github.com/getsentry/sentry-javascript/pull/18239)) - Bump @opentelemetry/context-async-hooks from ^2.1.0 to ^2.2.0 - Bump @opentelemetry/core from ^2.1.0 to ^2.2.0 From b8127fbec3ab3412931eb643911384fa77f5dd9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= <43071496+adam-kov@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:01:17 +0100 Subject: [PATCH 25/32] doc(sveltekit): Update documentation link for SvelteKit guide (#18298) Readme incorrectly pointed to NextJS docs --- packages/sveltekit/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sveltekit/README.md b/packages/sveltekit/README.md index a21adc43b836..a7a51e695255 100644 --- a/packages/sveltekit/README.md +++ b/packages/sveltekit/README.md @@ -25,7 +25,7 @@ functionality related to SvelteKit. ## Installation To get started installing the SDK, use the Sentry Next.js Wizard by running the following command in your terminal or -read the [Getting Started Docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/): +read the [Getting Started Docs](https://docs.sentry.io/platforms/javascript/guides/sveltekit/): ```sh npx @sentry/wizard@latest -i sveltekit From 3d48cc66730723653893ebab7d1b3edab6e9ff3c Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Mon, 24 Nov 2025 11:11:51 +0100 Subject: [PATCH 26/32] chore: Add external contributor to CHANGELOG.md (#18300) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #18298 Co-authored-by: Lms24 <8420481+Lms24@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac6b755cbb24..9bee97ac9189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @bignoncedric. Thank you for your contribution! +Work in this release was contributed by @bignoncedric and @adam-kov. Thank you for your contributions! - feat(deps): Bump OpenTelemetry ([#18239](https://github.com/getsentry/sentry-javascript/pull/18239)) - Bump @opentelemetry/context-async-hooks from ^2.1.0 to ^2.2.0 From 15256034ee8150a5b7dcb97d23eca1a5486f0cae Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:59:08 +0100 Subject: [PATCH 27/32] feat(browserprofiling): Add `manual` mode and deprecate old profiling (#18189) Adds the `manual` mode for profiling and browser integration tests. - adds deprecation note for old option - adds some JSDoc comments to public-facing API to make the difference between Node and UI profiling better visible. Closes https://github.com/getsentry/sentry-javascript/issues/17279 --- .../suites/profiling/manualMode/subject.js | 76 ++++ .../suites/profiling/manualMode/test.ts | 93 +++++ .../suites/profiling/test-utils.ts | 2 +- packages/browser/src/client.ts | 1 - packages/browser/src/exports.ts | 1 + packages/browser/src/profiling/UIProfiler.ts | 150 ++++--- packages/browser/src/profiling/index.ts | 55 +++ packages/browser/src/profiling/integration.ts | 23 +- packages/browser/src/profiling/utils.ts | 16 +- .../browser/test/profiling/UIProfiler.test.ts | 367 ++++++++++++++---- packages/core/src/client.ts | 28 ++ packages/core/src/profiling.ts | 5 + .../core/src/types-hoist/browseroptions.ts | 3 +- 13 files changed, 681 insertions(+), 139 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/profiling/manualMode/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/profiling/manualMode/test.ts create mode 100644 packages/browser/src/profiling/index.ts diff --git a/dev-packages/browser-integration-tests/suites/profiling/manualMode/subject.js b/dev-packages/browser-integration-tests/suites/profiling/manualMode/subject.js new file mode 100644 index 000000000000..906f14d06693 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/profiling/manualMode/subject.js @@ -0,0 +1,76 @@ +import * as Sentry from '@sentry/browser'; +import { browserProfilingIntegration } from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [browserProfilingIntegration()], + tracesSampleRate: 1, + profileSessionSampleRate: 1, + profileLifecycle: 'manual', +}); + +function largeSum(amount = 1000000) { + let sum = 0; + for (let i = 0; i < amount; i++) { + sum += Math.sqrt(i) * Math.sin(i); + } +} + +function fibonacci(n) { + if (n <= 1) { + return n; + } + return fibonacci(n - 1) + fibonacci(n - 2); +} + +function fibonacci1(n) { + if (n <= 1) { + return n; + } + return fibonacci1(n - 1) + fibonacci1(n - 2); +} + +function fibonacci2(n) { + if (n <= 1) { + return n; + } + return fibonacci1(n - 1) + fibonacci1(n - 2); +} + +function notProfiledFib(n) { + if (n <= 1) { + return n; + } + return fibonacci1(n - 1) + fibonacci1(n - 2); +} + +// Adding setTimeout to ensure we cross the sampling interval to avoid flakes + +Sentry.uiProfiler.startProfiler(); + +fibonacci(40); +await new Promise(resolve => setTimeout(resolve, 25)); + +largeSum(); +await new Promise(resolve => setTimeout(resolve, 25)); + +Sentry.uiProfiler.stopProfiler(); + +// --- + +notProfiledFib(40); +await new Promise(resolve => setTimeout(resolve, 25)); + +// --- + +Sentry.uiProfiler.startProfiler(); + +fibonacci2(40); +await new Promise(resolve => setTimeout(resolve, 25)); + +Sentry.uiProfiler.stopProfiler(); + +const client = Sentry.getClient(); +await client?.flush(8000); diff --git a/dev-packages/browser-integration-tests/suites/profiling/manualMode/test.ts b/dev-packages/browser-integration-tests/suites/profiling/manualMode/test.ts new file mode 100644 index 000000000000..2e4358563aa2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/profiling/manualMode/test.ts @@ -0,0 +1,93 @@ +import { expect } from '@playwright/test'; +import type { ProfileChunkEnvelope } from '@sentry/core'; +import { sentryTest } from '../../../utils/fixtures'; +import { + countEnvelopes, + getMultipleSentryEnvelopeRequests, + properFullEnvelopeRequestParser, + shouldSkipTracingTest, +} from '../../../utils/helpers'; +import { validateProfile, validateProfilePayloadMetadata } from '../test-utils'; + +sentryTest( + 'does not send profile envelope when document-policy is not set', + async ({ page, getLocalTestUrl, browserName }) => { + if (shouldSkipTracingTest() || browserName !== 'chromium') { + // Profiling only works when tracing is enabled + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + // Assert that no profile_chunk envelope is sent without policy header + const chunkCount = await countEnvelopes(page, { url, envelopeType: 'profile_chunk', timeout: 1500 }); + expect(chunkCount).toBe(0); + }, +); + +sentryTest('sends profile_chunk envelopes in manual mode', async ({ page, getLocalTestUrl, browserName }) => { + if (shouldSkipTracingTest() || browserName !== 'chromium') { + // Profiling only works when tracing is enabled + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname, responseHeaders: { 'Document-Policy': 'js-profiling' } }); + + // In manual mode we start and stop once -> expect exactly one chunk + const profileChunkEnvelopes = await getMultipleSentryEnvelopeRequests( + page, + 2, + { url, envelopeType: 'profile_chunk', timeout: 8000 }, + properFullEnvelopeRequestParser, + ); + + expect(profileChunkEnvelopes.length).toBe(2); + + // Validate the first chunk thoroughly + const profileChunkEnvelopeItem = profileChunkEnvelopes[0][1][0]; + const envelopeItemHeader = profileChunkEnvelopeItem[0]; + const envelopeItemPayload1 = profileChunkEnvelopeItem[1]; + + expect(envelopeItemHeader).toHaveProperty('type', 'profile_chunk'); + expect(envelopeItemPayload1.profile).toBeDefined(); + + const profilerId1 = envelopeItemPayload1.profiler_id; + + validateProfilePayloadMetadata(envelopeItemPayload1); + + validateProfile(envelopeItemPayload1.profile, { + expectedFunctionNames: ['startJSSelfProfile', 'fibonacci', 'largeSum'], + minSampleDurationMs: 20, + isChunkFormat: true, + }); + + // only contains fibonacci + const functionNames1 = envelopeItemPayload1.profile.frames.map(frame => frame.function).filter(name => name !== ''); + expect(functionNames1).toEqual(expect.not.arrayContaining(['fibonacci1', 'fibonacci2', 'fibonacci3'])); + + // === PROFILE CHUNK 2 === + + const profileChunkEnvelopeItem2 = profileChunkEnvelopes[1][1][0]; + const envelopeItemHeader2 = profileChunkEnvelopeItem2[0]; + const envelopeItemPayload2 = profileChunkEnvelopeItem2[1]; + + expect(envelopeItemHeader2).toHaveProperty('type', 'profile_chunk'); + expect(envelopeItemPayload2.profile).toBeDefined(); + + expect(envelopeItemPayload2.profiler_id).toBe(profilerId1); // same profiler id for the whole session + + validateProfilePayloadMetadata(envelopeItemPayload2); + + validateProfile(envelopeItemPayload2.profile, { + expectedFunctionNames: [ + 'startJSSelfProfile', + 'fibonacci1', // called by fibonacci2 + 'fibonacci2', + ], + isChunkFormat: true, + }); + + // does not contain notProfiledFib (called during unprofiled part) + const functionNames2 = envelopeItemPayload2.profile.frames.map(frame => frame.function).filter(name => name !== ''); + expect(functionNames2).toEqual(expect.not.arrayContaining(['notProfiledFib'])); +}); diff --git a/dev-packages/browser-integration-tests/suites/profiling/test-utils.ts b/dev-packages/browser-integration-tests/suites/profiling/test-utils.ts index e150be2d56bc..39e6d2ca20b7 100644 --- a/dev-packages/browser-integration-tests/suites/profiling/test-utils.ts +++ b/dev-packages/browser-integration-tests/suites/profiling/test-utils.ts @@ -90,7 +90,7 @@ export function validateProfile( } } - // Frames + // FRAMES expect(profile.frames.length).toBeGreaterThan(0); for (const frame of profile.frames) { expect(frame).toHaveProperty('function'); diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index ea55174f340c..65fcdf24734a 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -130,7 +130,6 @@ export class BrowserClient extends Client { // Flush logs and metrics when page becomes hidden (e.g., tab switch, navigation) // todo(v11): Remove the experimental flag - // eslint-disable-next-line deprecation/deprecation if (WINDOW.document && (sendClientReports || enableLogs || enableMetrics)) { WINDOW.document.addEventListener('visibilitychange', () => { if (WINDOW.document.visibilityState === 'hidden') { diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 50223e4b9fd9..1b46687194da 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -78,6 +78,7 @@ export { export { WINDOW } from './helpers'; export { BrowserClient } from './client'; export { makeFetchTransport } from './transports/fetch'; +export { uiProfiler } from './profiling'; export { defaultStackParser, defaultStackLineParsers, diff --git a/packages/browser/src/profiling/UIProfiler.ts b/packages/browser/src/profiling/UIProfiler.ts index 731684996d62..fb059b836986 100644 --- a/packages/browser/src/profiling/UIProfiler.ts +++ b/packages/browser/src/profiling/UIProfiler.ts @@ -1,4 +1,4 @@ -import type { Client, ProfileChunk, Span } from '@sentry/core'; +import type { Client, ContinuousProfiler, ProfileChunk, Span } from '@sentry/core'; import { type ProfileChunkEnvelope, createEnvelope, @@ -9,67 +9,122 @@ import { getSdkMetadataForEnvelopeHeader, uuid4, } from '@sentry/core'; +import type { BrowserOptions } from '../client'; import { DEBUG_BUILD } from './../debug-build'; import type { JSSelfProfiler } from './jsSelfProfiling'; -import { createProfileChunkPayload, startJSSelfProfile, validateProfileChunk } from './utils'; +import { createProfileChunkPayload, shouldProfileSession, startJSSelfProfile, validateProfileChunk } from './utils'; const CHUNK_INTERVAL_MS = 60_000; // 1 minute // Maximum length for trace lifecycle profiling per root span (e.g. if spanEnd never fires) -const MAX_ROOT_SPAN_PROFILE_MS = 300_000; // 5 minutes +const MAX_ROOT_SPAN_PROFILE_MS = 300_000; // 5 minutes max per root span in trace mode /** - * Browser trace-lifecycle profiler (UI Profiling / Profiling V2): - * - Starts when the first sampled root span starts - * - Stops when the last sampled root span ends - * - While running, periodically stops and restarts the JS self-profiling API to collect chunks + * UIProfiler (Profiling V2): + * Supports two lifecycle modes: + * - 'manual': controlled explicitly via start()/stop() + * - 'trace': automatically runs while there are active sampled root spans * * Profiles are emitted as standalone `profile_chunk` envelopes either when: * - there are no more sampled root spans, or * - the 60s chunk timer elapses while profiling is running. */ -export class UIProfiler { +export class UIProfiler implements ContinuousProfiler { private _client: Client | undefined; private _profiler: JSSelfProfiler | undefined; private _chunkTimer: ReturnType | undefined; - // For keeping track of active root spans + + // Manual + Trace + private _profilerId: string | undefined; // one per Profiler session + private _isRunning: boolean; // current profiler instance active flag + private _sessionSampled: boolean; // sampling decision for entire session + private _lifecycleMode: 'manual' | 'trace' | undefined; + + // Trace-only private _activeRootSpanIds: Set; private _rootSpanTimeouts: Map>; - // ID for Profiler session - private _profilerId: string | undefined; - private _isRunning: boolean; - private _sessionSampled: boolean; public constructor() { this._client = undefined; this._profiler = undefined; this._chunkTimer = undefined; - this._activeRootSpanIds = new Set(); - this._rootSpanTimeouts = new Map>(); + this._profilerId = undefined; this._isRunning = false; this._sessionSampled = false; + this._lifecycleMode = undefined; + + this._activeRootSpanIds = new Set(); + this._rootSpanTimeouts = new Map(); } /** - * Initialize the profiler with client and session sampling decision computed by the integration. + * Initialize the profiler with client, session sampling and lifecycle mode. */ - public initialize(client: Client, sessionSampled: boolean): void { - // One Profiler ID per profiling session (user session) - this._profilerId = uuid4(); + public initialize(client: Client): void { + const lifecycleMode = (client.getOptions() as BrowserOptions).profileLifecycle; + const sessionSampled = shouldProfileSession(client.getOptions()); + + DEBUG_BUILD && debug.log(`[Profiling] Initializing profiler (lifecycle='${lifecycleMode}').`); - DEBUG_BUILD && debug.log("[Profiling] Initializing profiler (lifecycle='trace')."); + if (!sessionSampled) { + DEBUG_BUILD && debug.log('[Profiling] Session not sampled. Skipping lifecycle profiler initialization.'); + } + // One Profiler ID per profiling session (user session) + this._profilerId = uuid4(); this._client = client; this._sessionSampled = sessionSampled; + this._lifecycleMode = lifecycleMode; - this._setupTraceLifecycleListeners(client); + if (lifecycleMode === 'trace') { + this._setupTraceLifecycleListeners(client); + } } - /** - * Handle an already-active root span at integration setup time. - */ - public notifyRootSpanActive(rootSpan: Span): void { + /** Starts UI profiling (only effective in 'manual' mode and when sampled). */ + public start(): void { + if (this._lifecycleMode === 'trace') { + DEBUG_BUILD && + debug.warn( + '[Profiling] `profileLifecycle` is set to "trace". Calls to `uiProfiler.start()` are ignored in trace mode.', + ); + return; + } + + if (this._isRunning) { + DEBUG_BUILD && debug.warn('[Profiling] Profile session is already running, `uiProfiler.start()` is a no-op.'); + return; + } + if (!this._sessionSampled) { + DEBUG_BUILD && debug.warn('[Profiling] Session is not sampled, `uiProfiler.start()` is a no-op.'); + return; + } + + this._beginProfiling(); + } + + /** Stops UI profiling (only effective in 'manual' mode). */ + public stop(): void { + if (this._lifecycleMode === 'trace') { + DEBUG_BUILD && + debug.warn( + '[Profiling] `profileLifecycle` is set to "trace". Calls to `uiProfiler.stop()` are ignored in trace mode.', + ); + return; + } + + if (!this._isRunning) { + DEBUG_BUILD && debug.warn('[Profiling] Profiler is not running, `uiProfiler.stop()` is a no-op.'); + return; + } + + this._endProfiling(); + } + + /** Handle an already-active root span at integration setup time (used only in trace mode). */ + public notifyRootSpanActive(rootSpan: Span): void { + if (this._lifecycleMode !== 'trace' || !this._sessionSampled) { return; } @@ -78,7 +133,7 @@ export class UIProfiler { return; } - this._activeRootSpanIds.add(spanId); + this._registerTraceRootSpan(spanId); const rootSpanCount = this._activeRootSpanIds.size; @@ -86,20 +141,20 @@ export class UIProfiler { DEBUG_BUILD && debug.log('[Profiling] Detected already active root span during setup. Active root spans now:', rootSpanCount); - this.start(); + this._beginProfiling(); } } /** - * Start profiling if not already running. + * Begin profiling if not already running. */ - public start(): void { + private _beginProfiling(): void { if (this._isRunning) { return; } this._isRunning = true; - DEBUG_BUILD && debug.log('[Profiling] Started profiling with profile ID:', this._profilerId); + DEBUG_BUILD && debug.log('[Profiling] Started profiling with profiler ID:', this._profilerId); // Expose profiler_id to match root spans with profiles getGlobalScope().setContext('profile', { profiler_id: this._profilerId }); @@ -107,7 +162,7 @@ export class UIProfiler { this._startProfilerInstance(); if (!this._profiler) { - DEBUG_BUILD && debug.log('[Profiling] Stopping trace lifecycle profiling.'); + DEBUG_BUILD && debug.log('[Profiling] Failed to start JS Profiler; stopping.'); this._resetProfilerInfo(); return; } @@ -115,15 +170,13 @@ export class UIProfiler { this._startPeriodicChunking(); } - /** - * Stop profiling; final chunk will be collected and sent. - */ - public stop(): void { + /** End profiling session; final chunk will be collected and sent. */ + private _endProfiling(): void { if (!this._isRunning) { return; } - this._isRunning = false; + if (this._chunkTimer) { clearTimeout(this._chunkTimer); this._chunkTimer = undefined; @@ -135,6 +188,12 @@ export class UIProfiler { this._collectCurrentChunk().catch(e => { DEBUG_BUILD && debug.error('[Profiling] Failed to collect current profile chunk on `stop()`:', e); }); + + // Manual: Clear profiling context so spans outside start()/stop() aren't marked as profiled + // Trace: Profile context is kept for the whole session duration + if (this._lifecycleMode === 'manual') { + getGlobalScope().setContext('profile', {}); + } } /** Trace-mode: attach spanStart/spanEnd listeners. */ @@ -166,7 +225,7 @@ export class UIProfiler { debug.log( `[Profiling] Root span ${spanId} started. Profiling active while there are active root spans (count=${rootSpanCount}).`, ); - this.start(); + this._beginProfiling(); } }); @@ -189,13 +248,13 @@ export class UIProfiler { this._collectCurrentChunk().catch(e => { DEBUG_BUILD && debug.error('[Profiling] Failed to collect current profile chunk on last `spanEnd`:', e); }); - this.stop(); + this._endProfiling(); } }); } /** - * Resets profiling information from scope and resets running state + * Resets profiling information from scope and resets running state (used on failure) */ private _resetProfilerInfo(): void { this._isRunning = false; @@ -210,7 +269,7 @@ export class UIProfiler { this._rootSpanTimeouts.clear(); } - /** Register root span and schedule safeguard timeout (trace mode). */ + /** Keep track of root spans and schedule safeguard timeout (trace mode). */ private _registerTraceRootSpan(spanId: string): void { this._activeRootSpanIds.add(spanId); const timeout = setTimeout(() => this._onRootSpanTimeout(spanId), MAX_ROOT_SPAN_PROFILE_MS); @@ -222,11 +281,11 @@ export class UIProfiler { */ private _startProfilerInstance(): void { if (this._profiler?.stopped === false) { - return; + return; // already running } const profiler = startJSSelfProfile(); if (!profiler) { - DEBUG_BUILD && debug.log('[Profiling] Failed to start JS Profiler in trace lifecycle.'); + DEBUG_BUILD && debug.log('[Profiling] Failed to start JS Profiler.'); return; } this._profiler = profiler; @@ -283,14 +342,13 @@ export class UIProfiler { this._activeRootSpanIds.delete(rootSpanId); - const rootSpanCount = this._activeRootSpanIds.size; - if (rootSpanCount === 0) { - this.stop(); + if (this._activeRootSpanIds.size === 0) { + this._endProfiling(); } } /** - * Stop the current profiler, convert and send a profile chunk. + * Stop current profiler instance, convert profile to chunk & send. */ private async _collectCurrentChunk(): Promise { const prevProfiler = this._profiler; diff --git a/packages/browser/src/profiling/index.ts b/packages/browser/src/profiling/index.ts new file mode 100644 index 000000000000..5847c070dd48 --- /dev/null +++ b/packages/browser/src/profiling/index.ts @@ -0,0 +1,55 @@ +import type { Profiler } from '@sentry/core'; +import { debug, getClient } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; + +/** + * Starts the Sentry UI profiler. + * This mode is exclusive with the transaction profiler and will only work if the profilesSampleRate is set to a falsy value. + * In UI profiling mode, the profiler will keep reporting profile chunks to Sentry until it is stopped, which allows for continuous profiling of the application. + */ +function startProfiler(): void { + const client = getClient(); + if (!client) { + DEBUG_BUILD && debug.warn('No Sentry client available, profiling is not started'); + return; + } + + const integration = client.getIntegrationByName('BrowserProfiling'); + + if (!integration) { + DEBUG_BUILD && debug.warn('BrowserProfiling integration is not available'); + return; + } + + client.emit('startUIProfiler'); +} + +/** + * Stops the Sentry UI profiler. + * Calls to stop will stop the profiler and flush the currently collected profile data to Sentry. + */ +function stopProfiler(): void { + const client = getClient(); + if (!client) { + DEBUG_BUILD && debug.warn('No Sentry client available, profiling is not started'); + return; + } + + const integration = client.getIntegrationByName('BrowserProfiling'); + if (!integration) { + DEBUG_BUILD && debug.warn('ProfilingIntegration is not available'); + return; + } + + client.emit('stopUIProfiler'); +} + +/** + * Profiler namespace for controlling the JS profiler in 'manual' mode. + * + * Requires the `browserProfilingIntegration` from the `@sentry/browser` package. + */ +export const uiProfiler: Profiler = { + startProfiler, + stopProfiler, +}; diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 7cd1886e636d..84cd33588320 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -14,7 +14,6 @@ import { getActiveProfilesCount, hasLegacyProfiling, isAutomatedPageLoadSpan, - shouldProfileSession, shouldProfileSpanLegacy, takeProfileFromGlobalCache, } from './utils'; @@ -26,12 +25,14 @@ const _browserProfilingIntegration = (() => { name: INTEGRATION_NAME, setup(client) { const options = client.getOptions() as BrowserOptions; + const profiler = new UIProfiler(); if (!hasLegacyProfiling(options) && !options.profileLifecycle) { // Set default lifecycle mode options.profileLifecycle = 'manual'; } + // eslint-disable-next-line deprecation/deprecation if (hasLegacyProfiling(options) && !options.profilesSampleRate) { DEBUG_BUILD && debug.log('[Profiling] Profiling disabled, no profiling options found.'); return; @@ -49,14 +50,15 @@ const _browserProfilingIntegration = (() => { // UI PROFILING (Profiling V2) if (!hasLegacyProfiling(options)) { - const sessionSampled = shouldProfileSession(options); - if (!sessionSampled) { - DEBUG_BUILD && debug.log('[Profiling] Session not sampled. Skipping lifecycle profiler initialization.'); - } - const lifecycleMode = options.profileLifecycle; - if (lifecycleMode === 'trace') { + // Registering hooks in all lifecycle modes to be able to notify users in case they want to start/stop the profiler manually in `trace` mode + client.on('startUIProfiler', () => profiler.start()); + client.on('stopUIProfiler', () => profiler.stop()); + + if (lifecycleMode === 'manual') { + profiler.initialize(client); + } else if (lifecycleMode === 'trace') { if (!hasSpansEnabled(options)) { DEBUG_BUILD && debug.warn( @@ -65,12 +67,11 @@ const _browserProfilingIntegration = (() => { return; } - const traceLifecycleProfiler = new UIProfiler(); - traceLifecycleProfiler.initialize(client, sessionSampled); + profiler.initialize(client); // If there is an active, sampled root span already, notify the profiler if (rootSpan) { - traceLifecycleProfiler.notifyRootSpanActive(rootSpan); + profiler.notifyRootSpanActive(rootSpan); } // In case rootSpan is created slightly after setup -> schedule microtask to re-check and notify. @@ -78,7 +79,7 @@ const _browserProfilingIntegration = (() => { const laterActiveSpan = getActiveSpan(); const laterRootSpan = laterActiveSpan && getRootSpan(laterActiveSpan); if (laterRootSpan) { - traceLifecycleProfiler.notifyRootSpanActive(laterRootSpan); + profiler.notifyRootSpanActive(laterRootSpan); } }, 0); } diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index ed794a40a98b..c50c76c84de4 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -651,8 +651,10 @@ export function shouldProfileSpanLegacy(span: Span): boolean { return false; } - // @ts-expect-error profilesSampleRate is not part of the browser options yet - const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate; + // eslint-disable-next-line deprecation/deprecation + const profilesSampleRate = (options as BrowserOptions).profilesSampleRate as + | BrowserOptions['profilesSampleRate'] + | boolean; // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The // only valid values are booleans or numbers between 0 and 1.) @@ -688,18 +690,21 @@ export function shouldProfileSpanLegacy(span: Span): boolean { } /** - * Determine if a profile should be created for the current session (lifecycle profiling mode). + * Determine if a profile should be created for the current session. */ export function shouldProfileSession(options: BrowserOptions): boolean { // If constructor failed once, it will always fail, so we can early return. if (PROFILING_CONSTRUCTOR_FAILED) { if (DEBUG_BUILD) { - debug.log('[Profiling] Profiling has been disabled for the duration of the current user session.'); + debug.log( + '[Profiling] Profiling has been disabled for the duration of the current user session as the JS Profiler could not be started.', + ); } return false; } - if (options.profileLifecycle !== 'trace') { + if (options.profileLifecycle !== 'trace' && options.profileLifecycle !== 'manual') { + DEBUG_BUILD && debug.warn('[Profiling] Session not sampled. Invalid `profileLifecycle` option.'); return false; } @@ -724,6 +729,7 @@ export function shouldProfileSession(options: BrowserOptions): boolean { * Checks if legacy profiling is configured. */ export function hasLegacyProfiling(options: BrowserOptions): boolean { + // eslint-disable-next-line deprecation/deprecation return typeof options.profilesSampleRate !== 'undefined'; } diff --git a/packages/browser/test/profiling/UIProfiler.test.ts b/packages/browser/test/profiling/UIProfiler.test.ts index f28880960256..6872e1e1beff 100644 --- a/packages/browser/test/profiling/UIProfiler.test.ts +++ b/packages/browser/test/profiling/UIProfiler.test.ts @@ -3,8 +3,20 @@ */ import * as Sentry from '@sentry/browser'; -import type { Span } from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { type Span, debug } from '@sentry/core'; +import { type Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { BrowserOptions } from '../../src/index'; + +function getBaseOptionsForTraceLifecycle(sendMock: Mock, enableTracing = true): BrowserOptions { + return { + dsn: 'https://public@o.ingest.sentry.io/1', + ...(enableTracing ? { tracesSampleRate: 1 } : {}), + profileSessionSampleRate: 1, + profileLifecycle: 'trace', + integrations: [Sentry.browserProfilingIntegration()], + transport: () => ({ flush: vi.fn().mockResolvedValue(true), send: sendMock }), + }; +} describe('Browser Profiling v2 trace lifecycle', () => { afterEach(async () => { @@ -48,12 +60,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { Sentry.init({ // tracing disabled - dsn: 'https://public@o.ingest.sentry.io/1', - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - // no tracesSampleRate/tracesSampler - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }), + ...getBaseOptionsForTraceLifecycle(send, false), }); // warning is logged by our debug logger only when DEBUG_BUILD, so just assert no throw and no profiler @@ -79,12 +86,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { const send = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }), + ...getBaseOptionsForTraceLifecycle(send), }); let spanRef: any; @@ -112,12 +114,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { const send = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }), + ...getBaseOptionsForTraceLifecycle(send), }); let spanA: any; @@ -159,12 +156,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { const send = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }), + ...getBaseOptionsForTraceLifecycle(send), }); let spanRef: any; @@ -195,12 +187,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { const send = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }), + ...getBaseOptionsForTraceLifecycle(send), }); let spanRef: any; @@ -255,12 +242,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { const send = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }), + ...getBaseOptionsForTraceLifecycle(send), }); Sentry.startSpanManual({ name: 'root-manual-never-ends', parentSpan: null, forceTransaction: true }, _span => { @@ -308,12 +290,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { const send = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }), + ...getBaseOptionsForTraceLifecycle(send), }); Sentry.startSpanManual({ name: 'root-manual-never-ends', parentSpan: null, forceTransaction: true }, _span => { @@ -375,12 +352,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { const send = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }), + ...getBaseOptionsForTraceLifecycle(send), }); Sentry.startSpan({ name: 'root-for-context', parentSpan: null, forceTransaction: true }, () => { @@ -440,12 +412,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { const send = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }), + ...getBaseOptionsForTraceLifecycle(send), }); Sentry.startSpan({ name: 'rootSpan-1', parentSpan: null, forceTransaction: true }, () => { @@ -499,12 +466,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { const send = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }), + ...getBaseOptionsForTraceLifecycle(send), }); Sentry.startSpan({ name: 'rootSpan-chunk-1', parentSpan: null, forceTransaction: true }, () => { @@ -563,12 +525,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { // Session 1 const send1 = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send: send1 }), + ...getBaseOptionsForTraceLifecycle(send1), }); Sentry.startSpan({ name: 'session-1-rootSpan', parentSpan: null, forceTransaction: true }, () => { @@ -598,12 +555,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { // Session 2 (new init simulates new user session) const send2 = vi.fn().mockResolvedValue(undefined); Sentry.init({ - dsn: 'https://public@o.ingest.sentry.io/1', - tracesSampleRate: 1, - profileSessionSampleRate: 1, - profileLifecycle: 'trace', - integrations: [Sentry.browserProfilingIntegration()], - transport: () => ({ flush: vi.fn().mockResolvedValue(true), send: send2 }), + ...getBaseOptionsForTraceLifecycle(send2), }); Sentry.startSpan({ name: 'session-2-rootSpan', parentSpan: null, forceTransaction: true }, () => { @@ -628,4 +580,271 @@ describe('Browser Profiling v2 trace lifecycle', () => { } }); }); + + it('calling start and stop in trace lifecycle prints warnings', async () => { + const { stop } = mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + const debugWarnSpy = vi.spyOn(debug, 'warn'); + + Sentry.init({ + ...getBaseOptionsForTraceLifecycle(send), + debug: true, + }); + + Sentry.uiProfiler.startProfiler(); + Sentry.uiProfiler.startProfiler(); + + expect(debugWarnSpy).toHaveBeenCalledWith( + '[Profiling] `profileLifecycle` is set to "trace". Calls to `uiProfiler.start()` are ignored in trace mode.', + ); + + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + debugWarnSpy.mockClear(); + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + expect(stop).toHaveBeenCalledTimes(0); + expect(debugWarnSpy).toHaveBeenCalledWith( + '[Profiling] `profileLifecycle` is set to "trace". Calls to `uiProfiler.stop()` are ignored in trace mode.', + ); + }); +}); + +function getBaseOptionsForManualLifecycle(sendMock: Mock, enableTracing = true): BrowserOptions { + return { + dsn: 'https://public@o.ingest.sentry.io/1', + ...(enableTracing ? { tracesSampleRate: 1 } : {}), + profileSessionSampleRate: 1, + profileLifecycle: 'manual', + integrations: [Sentry.browserProfilingIntegration()], + transport: () => ({ flush: vi.fn().mockResolvedValue(true), send: sendMock }), + }; +} + +describe('Browser Profiling v2 manual lifecycle', () => { + afterEach(async () => { + const client = Sentry.getClient(); + await client?.close(); + // reset profiler constructor + (window as any).Profiler = undefined; + vi.restoreAllMocks(); + }); + + function mockProfiler() { + const stop = vi.fn().mockResolvedValue({ + frames: [{ name: 'f' }], + stacks: [{ frameId: 0 }], + samples: [{ timestamp: 0 }, { timestamp: 10 }], + resources: [], + }); + + class MockProfilerImpl { + stopped: boolean = false; + constructor(_opts: { sampleInterval: number; maxBufferSize: number }) {} + stop() { + this.stopped = true; + return stop(); + } + addEventListener() {} + } + + const mockConstructor = vi.fn().mockImplementation((opts: { sampleInterval: number; maxBufferSize: number }) => { + return new MockProfilerImpl(opts); + }); + + (window as any).Profiler = mockConstructor; + return { stop, mockConstructor }; + } + + it('starts and stops a profile session', async () => { + const { stop, mockConstructor } = mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + + Sentry.init({ + ...getBaseOptionsForManualLifecycle(send), + }); + + const client = Sentry.getClient(); + expect(client).toBeDefined(); + + Sentry.uiProfiler.startProfiler(); + expect(mockConstructor).toHaveBeenCalledTimes(1); + + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + expect(stop).toHaveBeenCalledTimes(1); + expect(send).toHaveBeenCalledTimes(1); + const envelopeHeader = send.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]; + expect(envelopeHeader?.type).toBe('profile_chunk'); + }); + + it('calling start and stop while profile session is running prints warnings', async () => { + const { stop, mockConstructor } = mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + const debugWarnSpy = vi.spyOn(debug, 'warn'); + + Sentry.init({ + ...getBaseOptionsForManualLifecycle(send), + debug: true, + }); + + Sentry.uiProfiler.startProfiler(); + Sentry.uiProfiler.startProfiler(); + + expect(mockConstructor).toHaveBeenCalledTimes(1); + expect(debugWarnSpy).toHaveBeenCalledWith( + '[Profiling] Profile session is already running, `uiProfiler.start()` is a no-op.', + ); + + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + debugWarnSpy.mockClear(); + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + expect(stop).toHaveBeenCalledTimes(1); + expect(debugWarnSpy).toHaveBeenCalledWith('[Profiling] Profiler is not running, `uiProfiler.stop()` is a no-op.'); + }); + + it('profileSessionSampleRate is required', async () => { + const { stop, mockConstructor } = mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + const debugWarnSpy = vi.spyOn(debug, 'warn'); + + Sentry.init({ + ...getBaseOptionsForManualLifecycle(send), + profileSessionSampleRate: undefined, + }); + + Sentry.uiProfiler.startProfiler(); + expect(debugWarnSpy).toHaveBeenCalledWith( + '[Profiling] Invalid sample rate. Sample rate must be a boolean or a number between 0 and 1. Got undefined of type "undefined".', + ); + expect(debugWarnSpy).toHaveBeenCalledWith('[Profiling] Session is not sampled, `uiProfiler.start()` is a no-op.'); + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + expect(mockConstructor).not.toHaveBeenCalled(); + expect(stop).not.toHaveBeenCalled(); + }); + + it('does not start profiler when profileSessionSampleRate is 0', async () => { + const { stop, mockConstructor } = mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + + Sentry.init({ + ...getBaseOptionsForManualLifecycle(send), + profileSessionSampleRate: 0, + }); + + Sentry.uiProfiler.startProfiler(); + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + expect(mockConstructor).not.toHaveBeenCalled(); + expect(stop).not.toHaveBeenCalled(); + }); + + describe('envelope', () => { + beforeEach(() => { + vi.useRealTimers(); + }); + + it('sends a profile_chunk envelope type', async () => { + const stop = vi.fn().mockResolvedValue({ + frames: [{ name: 'f' }], + stacks: [{ frameId: 0 }], + samples: [{ timestamp: 0 }, { timestamp: 10 }], + resources: [], + }); + + class MockProfilerImpl { + stopped: boolean = false; + constructor(_opts: { sampleInterval: number; maxBufferSize: number }) {} + stop() { + this.stopped = true; + return stop(); + } + addEventListener() {} + } + + (window as any).Profiler = vi + .fn() + .mockImplementation((opts: { sampleInterval: number; maxBufferSize: number }) => new MockProfilerImpl(opts)); + + const send = vi.fn().mockResolvedValue(undefined); + + Sentry.init({ + ...getBaseOptionsForManualLifecycle(send), + }); + + const client = Sentry.getClient(); + + Sentry.uiProfiler.startProfiler(); + await new Promise(resolve => setTimeout(resolve, 10)); + Sentry.uiProfiler.stopProfiler(); + + await client?.flush(1000); + + expect(send.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ + type: 'profile_chunk', + }); + + expect(send.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ + profiler_id: expect.any(String), + chunk_id: expect.any(String), + profile: expect.objectContaining({ + stacks: expect.any(Array), + }), + }); + }); + + it('reuses the same profiler_id while profiling across multiple stop/start calls', async () => { + mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + + Sentry.init({ + ...getBaseOptionsForManualLifecycle(send), + }); + + // 1. profiling cycle + Sentry.uiProfiler.startProfiler(); + Sentry.startSpan({ name: 'manual-span-1', parentSpan: null, forceTransaction: true }, () => {}); + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + // Not profiled -> should not have profile context + Sentry.startSpan({ name: 'manual-span-between', parentSpan: null, forceTransaction: true }, () => {}); + + // 2. profiling cycle + Sentry.uiProfiler.startProfiler(); + Sentry.startSpan({ name: 'manual-span-2', parentSpan: null, forceTransaction: true }, () => {}); + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + const client = Sentry.getClient(); + await client?.flush(1000); + + const calls = send.mock.calls; + const transactionEvents = calls + .filter(call => call?.[0]?.[1]?.[0]?.[0]?.type === 'transaction') + .map(call => call?.[0]?.[1]?.[0]?.[1]); + + expect(transactionEvents.length).toBe(3); + + const firstProfilerId = transactionEvents[0]?.contexts?.profile?.profiler_id; + expect(typeof firstProfilerId).toBe('string'); + + // Middle transaction (not profiled) + expect(transactionEvents[1]?.contexts?.profile?.profiler_id).toBeUndefined(); + + const thirdProfilerId = transactionEvents[2]?.contexts?.profile?.profiler_id; + expect(typeof thirdProfilerId).toBe('string'); + expect(firstProfilerId).toBe(thirdProfilerId); // same profiler_id across session + }); + }); }); diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index b7e0cab509c1..ef05750009c3 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -813,6 +813,24 @@ export abstract class Client { callback: (request: unknown, response: unknown, normalizedRequest: RequestEventData) => void, ): () => void; + /** + * A hook that is called when the UI Profiler should start profiling. + * + * This hook is called when running `Sentry.uiProfiler.startProfiler()`. + * + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'startUIProfiler', callback: () => void): () => void; + + /** + * A hook that is called when the UI Profiler should stop profiling. + * + * This hook is called when running `Sentry.uiProfiler.stopProfiler()`. + * + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'stopUIProfiler', callback: () => void): () => void; + /** * Register a hook on this client. */ @@ -1029,6 +1047,16 @@ export abstract class Client { normalizedRequest: RequestEventData, ): void; + /** + * Emit a hook event for starting the UI Profiler. + */ + public emit(hook: 'startUIProfiler'): void; + + /** + * Emit a hook event for stopping the UI Profiler. + */ + public emit(hook: 'stopUIProfiler'): void; + /** * Emit a hook that was previously registered via `on()`. */ diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts index 407c4a07c53c..e2e2c34e38cc 100644 --- a/packages/core/src/profiling.ts +++ b/packages/core/src/profiling.ts @@ -65,6 +65,11 @@ function stopProfiler(): void { integration._profiler.stop(); } +/** + * Profiler namespace for controlling the profiler in 'manual' mode. + * + * Requires the `nodeProfilingIntegration` from the `@sentry/profiling-node` package. + */ export const profiler: Profiler = { startProfiler, stopProfiler, diff --git a/packages/core/src/types-hoist/browseroptions.ts b/packages/core/src/types-hoist/browseroptions.ts index 18bbd46af09c..39b414d5140b 100644 --- a/packages/core/src/types-hoist/browseroptions.ts +++ b/packages/core/src/types-hoist/browseroptions.ts @@ -18,10 +18,11 @@ export type BrowserClientReplayOptions = { }; export type BrowserClientProfilingOptions = { - // todo: add deprecation warning for profilesSampleRate: @deprecated Use `profileSessionSampleRate` and `profileLifecycle` instead. /** * The sample rate for profiling * 1.0 will profile all transactions and 0 will profile none. + * + * @deprecated Use `profileSessionSampleRate` and `profileLifecycle` instead. */ profilesSampleRate?: number; From 6240191d1fef08423b9928846fc4a9a7aa7c8da5 Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:53:32 +0100 Subject: [PATCH 28/32] feat(core): Use `maxValueLength` on error messages (#18301) It can happen that error messages are too long and exceed the maximum envelope size (mentioned in https://github.com/getsentry/sentry-javascript/issues/18219). `maxValueLength` now also checks for long error messages and truncates them. --- packages/core/src/utils/prepareEvent.ts | 13 ++++++++-- packages/core/test/lib/client.test.ts | 32 +++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 9a4c4685e839..fd1cb62440f4 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -147,8 +147,17 @@ export function applyClientOptions(event: Event, options: ClientOptions): void { } const request = event.request; - if (request?.url) { - request.url = maxValueLength ? truncate(request.url, maxValueLength) : request.url; + if (request?.url && maxValueLength) { + request.url = truncate(request.url, maxValueLength); + } + + if (maxValueLength) { + event.exception?.values?.forEach(exception => { + if (exception.value) { + // Truncates error messages + exception.value = truncate(exception.value, maxValueLength); + } + }); } } diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 19ef8a95dff5..2a2d77171880 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -21,7 +21,6 @@ import type { ErrorEvent, Event, TransactionEvent } from '../../src/types-hoist/ import type { SpanJSON } from '../../src/types-hoist/span'; import * as debugLoggerModule from '../../src/utils/debug-logger'; import * as miscModule from '../../src/utils/misc'; -import * as stringModule from '../../src/utils/string'; import * as timeModule from '../../src/utils/time'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; import { AdHocIntegration, AsyncTestIntegration, TestIntegration } from '../mocks/integration'; @@ -37,7 +36,6 @@ const clientProcess = vi.spyOn(TestClient.prototype as any, '_process'); vi.spyOn(miscModule, 'uuid4').mockImplementation(() => '12312012123120121231201212312012'); vi.spyOn(debugLoggerModule, 'consoleSandbox').mockImplementation(cb => cb()); -vi.spyOn(stringModule, 'truncate').mockImplementation(str => str); vi.spyOn(timeModule, 'dateTimestampInSeconds').mockImplementation(() => 2020); describe('Client', () => { @@ -263,6 +261,36 @@ describe('Client', () => { ); }); + test('does not truncate exception values by default', () => { + const exceptionMessageLength = 10_000; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + + client.captureException(new Error('a'.repeat(exceptionMessageLength))); + expect(TestClient.instance!.event).toEqual( + expect.objectContaining({ + exception: { + values: [{ type: 'Error', value: 'a'.repeat(exceptionMessageLength) }], + }, + }), + ); + }); + + test('truncates exception values according to `maxValueLength` option', () => { + const maxValueLength = 10; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, maxValueLength }); + const client = new TestClient(options); + + client.captureException(new Error('a'.repeat(50))); + expect(TestClient.instance!.event).toEqual( + expect.objectContaining({ + exception: { + values: [{ type: 'Error', value: `${'a'.repeat(maxValueLength)}...` }], + }, + }), + ); + }); + test('sets the correct lastEventId', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); From 4b92c64b75ffb85f31a516ca093ab77a5e55c15d Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 24 Nov 2025 17:41:05 +0200 Subject: [PATCH 29/32] fix(nextjs): universal random tunnel path support (#18257) Summary for changelog: The `tunnelRoute: true` option didn't work well with Turbopack due to repeated runs of the config files leading to different tunnel URLs in client, server and edge runtimes, this PR fixes that while also fixing Sentry requests spans not being dropped by the sampler. When using Next.js with Turbopack and the Sentry tunnel route feature (`tunnelRoute: true`), several issues prevented events from being sent properly: ### 1. Tunnel Route Consistency (Turbopack) **Problem**: Random tunnel routes were generated separately for client and server builds in Turbopack. **Solution**: Implemented processs-level caching in `withSentryConfig.ts`: - Extract tunnel route resolution into `resolveTunnelRoute()` function - Use `process.env` to store the random tunnel value across server/client builds. ### 2. Filter Tunnel Request Spans **Problem**: Requests to the tunnel route (before rewrite) and to Sentry ingest URLs (after rewrite) were creating spans that polluted Sentry with internal instrumentation noise, spans were being created by the middleware and OTEL node.js fetch instrumentation. **Solution**: Implemented server-side span filtering: - Created `dropMiddlewareTunnelRequests()` utility to detect and drop tunnel-related spans - Filter spans originating from `Middleware.execute` (Next.js middleware) - Filter spans originating from `auto.http.otel.node_fetch` (Node.js fetch instrumentation) - Check both local tunnel paths and Sentry ingest URLs (using `isSentryRequestSpan` from `@sentry/opentelemetry`) - Mark matching spans with `TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION` to prevent them from being sent - I tried `beforeSampling` hook but it didn't work for some reason, so I stuck with the drop attribute. ---- The final issue was excluding the tunnel requests from the middleware/proxy, but there are many blockers for a solution: 1. The `config` must be statically analyzable, so we cannot expose `withSentryMiddlewareConfig` wrapper of any kind. 2. Warning the user doesn't help much because they can't do anything about it since the tunnel route is random. 3. Tested out writing a loader for turbopack/webpack to inject the tunnel into the matcher as an array but user existing matcher can match still. 4. Only way is to inject an exclusion match into the user existing matcher, if it is an array then we need to inject it into each single entry. I may explore this further later with a loader for both webpack/turbopack, and figure out a reliable way to inject the negative matchers into the user expressions. --- .../nextjs-16-tunnel/.gitignore | 46 ++++++ .../test-applications/nextjs-16-tunnel/.npmrc | 4 + .../nextjs-16-tunnel/app/favicon.ico | Bin 0 -> 25931 bytes .../nextjs-16-tunnel/app/global-error.tsx | 23 +++ .../nextjs-16-tunnel/app/layout.tsx | 7 + .../nextjs-16-tunnel/app/page.tsx | 3 + .../nextjs-16-tunnel/eslint.config.mjs | 19 +++ .../instrumentation-client.ts | 12 ++ .../nextjs-16-tunnel/instrumentation.ts | 13 ++ .../nextjs-16-tunnel/next.config.ts | 9 ++ .../nextjs-16-tunnel/package.json | 63 +++++++++ .../nextjs-16-tunnel/playwright.config.mjs | 29 ++++ .../nextjs-16-tunnel/proxy.ts | 11 ++ .../nextjs-16-tunnel/public/file.svg | 1 + .../nextjs-16-tunnel/public/globe.svg | 1 + .../nextjs-16-tunnel/public/next.svg | 1 + .../nextjs-16-tunnel/public/vercel.svg | 1 + .../nextjs-16-tunnel/public/window.svg | 1 + .../nextjs-16-tunnel/sentry.edge.config.ts | 11 ++ .../nextjs-16-tunnel/sentry.server.config.ts | 11 ++ .../nextjs-16-tunnel/start-event-proxy.mjs | 14 ++ .../tests/tunnel-route.test.ts | 132 ++++++++++++++++++ .../nextjs-16-tunnel/tsconfig.json | 27 ++++ .../utils/dropMiddlewareTunnelRequests.ts | 59 ++++++++ .../turbopack/constructTurbopackConfig.ts | 12 +- .../turbopack/generateValueInjectionRules.ts | 7 + .../nextjs/src/config/withSentryConfig.ts | 39 +++++- packages/nextjs/src/edge/index.ts | 24 ++++ packages/nextjs/src/server/index.ts | 3 + 29 files changed, 576 insertions(+), 7 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/global-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/eslint.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation-client.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/proxy.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/file.svg create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/globe.svg create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/next.svg create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/vercel.svg create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/window.svg create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.edge.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tsconfig.json create mode 100644 packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.gitignore new file mode 100644 index 000000000000..ae044ec5ad53 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.gitignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +event-dumps + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Sentry Config File +.env.sentry-build-plugin diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc new file mode 100644 index 000000000000..a3160f4de175 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc @@ -0,0 +1,4 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 +public-hoist-pattern[]=*import-in-the-middle* +public-hoist-pattern[]=*require-in-the-middle* diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/favicon.ico b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/global-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/global-error.tsx new file mode 100644 index 000000000000..20c175015b03 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/global-error.tsx @@ -0,0 +1,23 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; +import NextError from 'next/error'; +import { useEffect } from 'react'; + +export default function GlobalError({ error }: { error: Error & { digest?: string } }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + {/* `NextError` is the default Next.js error page component. Its type + definition requires a `statusCode` prop. However, since the App Router + does not expose status codes for errors, we simply pass 0 to render a + generic error message. */} + + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/layout.tsx new file mode 100644 index 000000000000..c8f9cee0b787 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/page.tsx new file mode 100644 index 000000000000..f28a670096bf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Next.js 16 Tunnel Route Test

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/eslint.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/eslint.config.mjs new file mode 100644 index 000000000000..60f7af38f6c2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/eslint.config.mjs @@ -0,0 +1,19 @@ +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { FlatCompat } from '@eslint/eslintrc'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + { + ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'], + }, +]; + +export default eslintConfig; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation-client.ts new file mode 100644 index 000000000000..d40b790f18a5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation-client.ts @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + // Use a fake but properly formatted Sentry SaaS DSN for tunnel route testing + dsn: 'https://public@o12345.ingest.us.sentry.io/67890', + // No tunnel option - using tunnelRoute from withSentryConfig + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); + +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation.ts new file mode 100644 index 000000000000..964f937c439a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/nextjs'; + +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('./sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('./sentry.edge.config'); + } +} + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts new file mode 100644 index 000000000000..cad68b926a58 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts @@ -0,0 +1,9 @@ +import { withSentryConfig } from '@sentry/nextjs'; +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = {}; + +export default withSentryConfig(nextConfig, { + silent: true, + tunnelRoute: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json new file mode 100644 index 000000000000..40389ad0888f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json @@ -0,0 +1,63 @@ +{ + "name": "nextjs-16-tunnel", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": " next dev", + "build": "_SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs", + "dev:webpack": "_SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ next dev --webpack", + "build-webpack": "_SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ next build --webpack > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "start": "_SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ next start", + "lint": "eslint", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development _SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ playwright test", + "test:dev-webpack": "TEST_ENV=development-webpack _SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ playwright test", + "test:build": "pnpm install && pnpm build", + "test:build-webpack": "pnpm install && pnpm build-webpack", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && pnpm build", + "test:build-latest-webpack": "pnpm install && pnpm add next@latest && pnpm build-webpack", + "test:build-canary-webpack": "pnpm install && pnpm add next@canary && pnpm build-webpack", + "test:assert": "pnpm test:prod && pnpm test:dev", + "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack" + }, + "dependencies": { + "@sentry/nextjs": "latest || *", + "@sentry/core": "latest || *", + "ai": "^3.0.0", + "import-in-the-middle": "^1", + "next": "16.0.0", + "react": "19.1.0", + "react-dom": "19.1.0", + "require-in-the-middle": "^7", + "zod": "^3.22.4" + }, + "devDependencies": { + "@playwright/test": "~1.53.2", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "canary", + "typescript": "^5" + }, + "volta": { + "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "pnpm test:build-webpack", + "label": "nextjs-16-tunnel (webpack)", + "assert-command": "pnpm test:assert-webpack" + }, + { + "build-command": "pnpm test:build", + "label": "nextjs-16-tunnel (turbopack)", + "assert-command": "pnpm test:assert" + } + ] + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/playwright.config.mjs new file mode 100644 index 000000000000..797418b8cf7d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/playwright.config.mjs @@ -0,0 +1,29 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const getStartCommand = () => { + if (testEnv === 'development-webpack') { + return 'pnpm next dev -p 3030 --webpack 2>&1 | tee .tmp_dev_server_logs'; + } + + if (testEnv === 'development') { + return 'pnpm next dev -p 3030 2>&1 | tee .tmp_dev_server_logs'; + } + + if (testEnv === 'production') { + return 'pnpm next start -p 3030'; + } + + throw new Error(`Unknown test env: ${testEnv}`); +}; + +const config = getPlaywrightConfig({ + startCommand: getStartCommand(), + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/proxy.ts new file mode 100644 index 000000000000..28639f60bbe4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/proxy.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +export async function proxy(_request: NextRequest) { + return NextResponse.next(); +} + +// Match all routes to test that tunnel requests are properly filtered +export const config = { + matcher: '/:path*', +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/file.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/file.svg new file mode 100644 index 000000000000..004145cddf3f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/globe.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/globe.svg new file mode 100644 index 000000000000..567f17b0d7c7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/next.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/next.svg new file mode 100644 index 000000000000..5174b28c565c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/vercel.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/vercel.svg new file mode 100644 index 000000000000..77053960334e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/window.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/window.svg new file mode 100644 index 000000000000..b2b2a44f6ebc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.edge.config.ts new file mode 100644 index 000000000000..8ba3a3bf2faa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.edge.config.ts @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + // Use a fake but properly formatted Sentry SaaS DSN for tunnel route testing + dsn: 'https://public@o12345.ingest.us.sentry.io/67890', + // No tunnel option - using tunnelRoute from withSentryConfig + tracesSampleRate: 1.0, + sendDefaultPii: true, + // debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts new file mode 100644 index 000000000000..8ba3a3bf2faa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + // Use a fake but properly formatted Sentry SaaS DSN for tunnel route testing + dsn: 'https://public@o12345.ingest.us.sentry.io/67890', + // No tunnel option - using tunnelRoute from withSentryConfig + tracesSampleRate: 1.0, + sendDefaultPii: true, + // debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/start-event-proxy.mjs new file mode 100644 index 000000000000..976073d3d2c4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/start-event-proxy.mjs @@ -0,0 +1,14 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'))); + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nextjs-16-tunnel', + envelopeDumpPath: path.join( + process.cwd(), + `event-dumps/nextjs-16-tunnel-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + ), +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts new file mode 100644 index 000000000000..a8bd7b4d925e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts @@ -0,0 +1,132 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Tunnel route should proxy pageload transaction to Sentry', async ({ page }) => { + // Wait for the pageload transaction to be sent through the tunnel + const pageloadTransactionPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + // Navigate to the page + await page.goto('/'); + + const pageloadTransaction = await pageloadTransactionPromise; + + // Verify the pageload transaction was received successfully + expect(pageloadTransaction).toBeDefined(); + expect(pageloadTransaction.transaction).toBe('/'); + expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload'); + expect(pageloadTransaction.contexts?.trace?.status).toBe('ok'); + expect(pageloadTransaction.type).toBe('transaction'); +}); + +test('Tunnel route should send multiple pageload transactions consistently', async ({ page }) => { + // This test verifies that the tunnel route remains consistent across multiple page loads + // (important for Turbopack which could generate different tunnel routes for client/server) + + // First pageload + const firstPageloadPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.goto('/'); + const firstPageload = await firstPageloadPromise; + + expect(firstPageload).toBeDefined(); + expect(firstPageload.transaction).toBe('/'); + expect(firstPageload.contexts?.trace?.op).toBe('pageload'); + expect(firstPageload.contexts?.trace?.status).toBe('ok'); + + // Second pageload (reload) + const secondPageloadPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.reload(); + const secondPageload = await secondPageloadPromise; + + expect(secondPageload).toBeDefined(); + expect(secondPageload.transaction).toBe('/'); + expect(secondPageload.contexts?.trace?.op).toBe('pageload'); + expect(secondPageload.contexts?.trace?.status).toBe('ok'); +}); + +test('Tunnel requests should not create middleware or fetch spans', async ({ page }) => { + // This test verifies that our span filtering logic works correctly + // The proxy runs on all routes, so we'll get a middleware transaction for `/` + // But we should NOT get middleware or fetch transactions for the tunnel route itself + + const allTransactions: any[] = []; + + // Collect all transactions + const collectPromise = (async () => { + // Keep collecting for 3 seconds after pageload + const endTime = Date.now() + 3000; + while (Date.now() < endTime) { + try { + const tx = await Promise.race([ + waitForTransaction('nextjs-16-tunnel', () => true), + new Promise((_, reject) => setTimeout(() => reject(), 500)), + ]); + allTransactions.push(tx); + } catch { + // Timeout, continue collecting + } + } + })(); + + // Wait for pageload transaction + const pageloadPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + const pageloadTransaction = await pageloadPromise; + + // Trigger errors to force tunnel POST requests + await page + .evaluate(() => { + throw new Error('Test tunnel error 1'); + }) + .catch(() => { + // Expected to throw + }); + + await page + .evaluate(() => { + throw new Error('Test tunnel error 2'); + }) + .catch(() => { + // Expected to throw + }); + + // Wait for events to be sent through tunnel + await page.waitForTimeout(2000); + + // Continue collecting for a bit + await collectPromise; + + // We should have received the pageload transaction + expect(pageloadTransaction).toBeDefined(); + expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload'); + + const middlewareTransactions = allTransactions.filter(tx => tx.contexts?.trace?.op === 'http.server.middleware'); + + // We WILL have a middleware transaction for GET / (the pageload) + // But we should NOT have middleware transactions for POST requests (tunnel route) + const postMiddlewareTransactions = middlewareTransactions.filter( + tx => tx.transaction?.includes('POST') || tx.contexts?.trace?.data?.['http.request.method'] === 'POST', + ); + + expect(postMiddlewareTransactions).toHaveLength(0); + + // We should NOT have any fetch transactions to Sentry ingest + const sentryFetchTransactions = allTransactions.filter( + tx => + tx.contexts?.trace?.op === 'http.client' && + (tx.contexts?.trace?.data?.['url.full']?.includes('sentry.io') || + tx.contexts?.trace?.data?.['url.full']?.includes('ingest')), + ); + + expect(sentryFetchTransactions).toHaveLength(0); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tsconfig.json new file mode 100644 index 000000000000..cc9ed39b5aa2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts"], + "exclude": ["node_modules"] +} diff --git a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts new file mode 100644 index 000000000000..6f8b4eb96603 --- /dev/null +++ b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts @@ -0,0 +1,59 @@ +import { SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions'; +import { type Span, type SpanAttributes, GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { isSentryRequestSpan } from '@sentry/opentelemetry'; +import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; +import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached'; + +const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { + _sentryRewritesTunnelPath?: string; +}; + +/** + * Drops spans for tunnel requests from middleware or fetch instrumentation. + * This catches both: + * 1. Requests to the local tunnel route (before rewrite) + * 2. Requests to Sentry ingest (after rewrite) + */ +export function dropMiddlewareTunnelRequests(span: Span, attrs: SpanAttributes | undefined): void { + // Only filter middleware spans or HTTP fetch spans + const isMiddleware = attrs?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute'; + // The fetch span could be originating from rewrites re-writing a tunnel request + // So we want to filter it out + const isFetchSpan = attrs?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.http.otel.node_fetch'; + + // If the span is not a middleware span or a fetch span, return + if (!isMiddleware && !isFetchSpan) { + return; + } + + // Check if this is either a tunnel route request or a Sentry ingest request + const isTunnel = isTunnelRouteSpan(attrs || {}); + const isSentry = isSentryRequestSpan(span); + + if (isTunnel || isSentry) { + // Mark the span to be dropped + span.setAttribute(TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, true); + } +} + +/** + * Checks if a span's HTTP target matches the tunnel route. + */ +function isTunnelRouteSpan(spanAttributes: Record): boolean { + const tunnelPath = globalWithInjectedValues._sentryRewritesTunnelPath || process.env._sentryRewritesTunnelPath; + if (!tunnelPath) { + return false; + } + + // eslint-disable-next-line deprecation/deprecation + const httpTarget = spanAttributes[SEMATTRS_HTTP_TARGET]; + + if (typeof httpTarget === 'string') { + // Extract pathname from the target (e.g., "/tunnel?o=123&p=456" -> "/tunnel") + const pathname = httpTarget.split('?')[0] || ''; + + return pathname.startsWith(tunnelPath); + } + + return false; +} diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts index e46d3f6bb5c7..b96b8e7f77ee 100644 --- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts +++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts @@ -20,23 +20,31 @@ export function constructTurbopackConfig({ nextJsVersion, }: { userNextConfig: NextConfigObject; - userSentryOptions: SentryBuildOptions; + userSentryOptions?: SentryBuildOptions; routeManifest?: RouteManifest; nextJsVersion?: string; }): TurbopackOptions { // If sourcemaps are disabled, we don't need to enable native debug ids as this will add build time. const shouldEnableNativeDebugIds = (supportsNativeDebugIds(nextJsVersion ?? '') && userNextConfig?.turbopack?.debugIds) ?? - userSentryOptions.sourcemaps?.disable !== true; + userSentryOptions?.sourcemaps?.disable !== true; const newConfig: TurbopackOptions = { ...userNextConfig.turbopack, ...(shouldEnableNativeDebugIds ? { debugIds: true } : {}), }; + const tunnelPath = + userSentryOptions?.tunnelRoute !== undefined && + userNextConfig.output !== 'export' && + typeof userSentryOptions.tunnelRoute === 'string' + ? `${userNextConfig.basePath ?? ''}${userSentryOptions.tunnelRoute}` + : undefined; + const valueInjectionRules = generateValueInjectionRules({ routeManifest, nextJsVersion, + tunnelPath, }); for (const { matcher, rule } of valueInjectionRules) { diff --git a/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts b/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts index 58cf7cdd0a15..2cf96b5f5ad7 100644 --- a/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts +++ b/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts @@ -8,9 +8,11 @@ import type { JSONValue, TurbopackMatcherWithRule } from '../types'; export function generateValueInjectionRules({ routeManifest, nextJsVersion, + tunnelPath, }: { routeManifest?: RouteManifest; nextJsVersion?: string; + tunnelPath?: string; }): TurbopackMatcherWithRule[] { const rules: TurbopackMatcherWithRule[] = []; const isomorphicValues: Record = {}; @@ -26,6 +28,11 @@ export function generateValueInjectionRules({ clientValues._sentryRouteManifest = JSON.stringify(routeManifest); } + // Inject tunnel route path for both client and server + if (tunnelPath) { + isomorphicValues._sentryRewritesTunnelPath = tunnelPath; + } + if (Object.keys(isomorphicValues).length > 0) { clientValues = { ...clientValues, ...isomorphicValues }; serverValues = { ...serverValues, ...isomorphicValues }; diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 7ac61d73aa73..892f4d6745fa 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -121,11 +121,10 @@ function getFinalConfigObject( ); } } else { - const resolvedTunnelRoute = - userSentryOptions.tunnelRoute === true ? generateRandomTunnelRoute() : userSentryOptions.tunnelRoute; - // Update the global options object to use the resolved value everywhere + const resolvedTunnelRoute = resolveTunnelRoute(userSentryOptions.tunnelRoute); userSentryOptions.tunnelRoute = resolvedTunnelRoute || undefined; + setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); } } @@ -392,6 +391,13 @@ function getFinalConfigObject( */ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: string): void { const originalRewrites = userNextConfig.rewrites; + // Allow overriding the tunnel destination for E2E tests via environment variable + const destinationOverride = process.env._SENTRY_TUNNEL_DESTINATION_OVERRIDE; + + // Make sure destinations are statically defined at build time + const destination = destinationOverride || 'https://o:orgid.ingest.sentry.io/api/:projectid/envelope/?hsts=0'; + const destinationWithRegion = + destinationOverride || 'https://o:orgid.ingest.:region.sentry.io/api/:projectid/envelope/?hsts=0'; // This function doesn't take any arguments at the time of writing but we future-proof // here in case Next.js ever decides to pass some @@ -412,7 +418,7 @@ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: s value: '(?\\d*)', }, ], - destination: 'https://o:orgid.ingest.sentry.io/api/:projectid/envelope/?hsts=0', + destination, }; const tunnelRouteRewriteWithRegion = { @@ -436,7 +442,7 @@ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: s value: '(?[a-z]{2})', }, ], - destination: 'https://o:orgid.ingest.:region.sentry.io/api/:projectid/envelope/?hsts=0', + destination: destinationWithRegion, }; // Order of these is important, they get applied first to last. @@ -550,3 +556,26 @@ function getInstrumentationClientFileContents(): string | void { } } } + +/** + * Resolves the tunnel route based on the user's configuration and the environment. + * @param tunnelRoute - The user-provided tunnel route option + */ +function resolveTunnelRoute(tunnelRoute: string | true): string { + if (process.env.__SENTRY_TUNNEL_ROUTE__) { + // Reuse cached value from previous build (server/client) + return process.env.__SENTRY_TUNNEL_ROUTE__; + } + + const resolvedTunnelRoute = typeof tunnelRoute === 'string' ? tunnelRoute : generateRandomTunnelRoute(); + + // Cache for subsequent builds (only during build time) + // Turbopack runs the config twice, so we need a shared context to avoid generating a new tunnel route for each build. + // env works well here + // https://linear.app/getsentry/issue/JS-549/adblock-plus-blocking-requests-to-sentry-and-monitoring-tunnel + if (resolvedTunnelRoute) { + process.env.__SENTRY_TUNNEL_ROUTE__ = resolvedTunnelRoute; + } + + return resolvedTunnelRoute; +} diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 5fd92707b912..091adab98dee 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,5 +1,6 @@ import { context } from '@opentelemetry/api'; import { + type EventProcessor, applySdkMetadata, getCapturedScopesOnSpan, getCurrentScope, @@ -19,7 +20,9 @@ import { import { getScopesFromContext } from '@sentry/opentelemetry'; import type { VercelEdgeOptions } from '@sentry/vercel-edge'; import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge'; +import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; +import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; import { isBuild } from '../common/utils/isBuild'; import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; @@ -35,6 +38,7 @@ export type EdgeOptions = VercelEdgeOptions; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesDistDir?: string; _sentryRelease?: string; + _sentryRewritesTunnelPath?: string; }; /** Inits the Sentry NextJS SDK on the Edge Runtime. */ @@ -70,6 +74,8 @@ export function init(options: VercelEdgeOptions = {}): void { const rootSpan = getRootSpan(span); const isRootSpan = span === rootSpan; + dropMiddlewareTunnelRequests(span, spanAttributes); + // Mark all spans generated by Next.js as 'auto' if (spanAttributes?.['next.span_type'] !== undefined) { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); @@ -137,6 +143,24 @@ export function init(options: VercelEdgeOptions = {}): void { } }); + getGlobalScope().addEventProcessor( + Object.assign( + (event => { + // Filter transactions that we explicitly want to drop. + if (event.type === 'transaction') { + if (event.contexts?.trace?.data?.[TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]) { + return null; + } + + return event; + } else { + return event; + } + }) satisfies EventProcessor, + { id: 'NextLowQualityTransactionsFilter' }, + ), + ); + try { // @ts-expect-error `process.turbopack` is a magic string that will be replaced by Next.js if (process.turbopack) { diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index ce8ac7c56cea..caec9a9f1af1 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -38,6 +38,7 @@ import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, } from '../common/span-attributes-with-logic-attached'; import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; +import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; import { isBuild } from '../common/utils/isBuild'; import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; @@ -169,6 +170,8 @@ export function init(options: NodeOptions): NodeClient | undefined { const rootSpan = getRootSpan(span); const isRootSpan = span === rootSpan; + dropMiddlewareTunnelRequests(span, spanAttributes); + // What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted // by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute. if (typeof spanAttributes?.[ATTR_NEXT_ROUTE] === 'string') { From 235c8651356fb66b6214ff5b88f16419c65f3478 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 24 Nov 2025 17:18:17 +0100 Subject: [PATCH 30/32] feat(core): Re-add `_experiments.enableLogs` option (#18299) We're re-introducing `_experiments.enableLogs`. The option stays deprecated and maybe we can actually remove it or type it as `undefined` in the next major to sunset it for good. Main motivation for re-adding: The flag was introduced in v9 while we already worked on v10 where we removed it again. Therefore, it had an unusually short lifespan. Some users didn't realize this when upgrading to v10 and were wondering where their logs went. --- .../suites/public-api/logger/init.js | 5 +++- .../public-api/logger/integration/init.js | 5 ++++ .../suites/winston/subject.ts | 6 +++- packages/core/src/client.ts | 5 ++++ packages/core/src/types-hoist/options.ts | 8 +++++ packages/core/test/lib/client.test.ts | 30 +++++++++++++++++++ 6 files changed, 57 insertions(+), 2 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/init.js index 8026df91ea46..1fa010f49659 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/init.js @@ -4,5 +4,8 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - enableLogs: true, + // purposefully testing against the experimental flag here + _experiments: { + enableLogs: true, + }, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js index 809b78739e77..e26b03d7fc61 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js @@ -5,5 +5,10 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', enableLogs: true, + // Purposefully specifying the experimental flag here + // to ensure the top level option is used instead. + _experiments: { + enableLogs: false, + }, integrations: [Sentry.consoleLoggingIntegration()], }); diff --git a/dev-packages/node-core-integration-tests/suites/winston/subject.ts b/dev-packages/node-core-integration-tests/suites/winston/subject.ts index 3c31ddb63fa5..02ffcdb0f5cb 100644 --- a/dev-packages/node-core-integration-tests/suites/winston/subject.ts +++ b/dev-packages/node-core-integration-tests/suites/winston/subject.ts @@ -8,7 +8,11 @@ const client = Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', - enableLogs: true, + // Purposefully specifying the experimental flag here + // to ensure the top level option is still respected. + _experiments: { + enableLogs: true, + }, transport: loggingTransport, }); diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index ef05750009c3..805d8e596528 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -239,6 +239,11 @@ export abstract class Client { }); } + // Backfill enableLogs option from _experiments.enableLogs + // TODO(v11): Remove or change default value + // eslint-disable-next-line deprecation/deprecation + this._options.enableLogs = this._options.enableLogs ?? this._options._experiments?.enableLogs; + // Setup log flushing with weight and timeout tracking if (this._options.enableLogs) { setupWeightBasedFlushing(this, 'afterCaptureLog', 'flushLogs', estimateLogSizeInBytes, _INTERNAL_flushLogsBuffer); diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 59c4609f01c4..c33d0107df5f 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -306,6 +306,14 @@ export interface ClientOptions Metric | null; + + /** + * Determines if logs support should be enabled. + * + * @default false + * @deprecated Use the top level `enableLogs` option instead. + */ + enableLogs?: boolean; }; /** diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 2a2d77171880..a59a8bbb8780 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -2734,6 +2734,36 @@ describe('Client', () => { }); }); + describe('enableLogs', () => { + it('defaults to `undefined`', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + expect(client.getOptions().enableLogs).toBeUndefined(); + }); + + it('can be set as a top-level option', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + expect(client.getOptions().enableLogs).toBe(true); + }); + + it('can be set as an experimental option', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableLogs: true } }); + const client = new TestClient(options); + expect(client.getOptions().enableLogs).toBe(true); + }); + + test('top-level option takes precedence over experimental option', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + _experiments: { enableLogs: false }, + }); + const client = new TestClient(options); + expect(client.getOptions().enableLogs).toBe(true); + }); + }); + describe('log weight-based flushing', () => { beforeEach(() => { vi.useFakeTimers(); From 6ce620e983814a263eb036a3ee79f80e780a880a Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:31:48 +0100 Subject: [PATCH 31/32] fix(core): Always redact content of sensitive headers regardless of `sendDefaultPii` (#18311) In case an HTTP header is considered "sensitive" (could contain tokens), the value is already filtered within the SDK. --- Follow-up on this PR: - https://github.com/getsentry/sentry-javascript/pull/17475 --- packages/astro/src/server/middleware.ts | 5 +- packages/bun/src/integrations/bunserver.ts | 5 +- packages/cloudflare/src/request.ts | 3 +- packages/core/src/utils/request.ts | 39 ++++++---- packages/core/test/lib/utils/request.test.ts | 76 ++++++++----------- .../common/utils/addHeadersAsAttributes.ts | 7 +- .../http/httpServerSpansIntegration.ts | 3 +- .../runtime/hooks/wrapMiddlewareHandler.ts | 7 +- packages/remix/src/server/instrumentServer.ts | 5 +- .../sveltekit/src/server-common/handle.ts | 11 +-- 10 files changed, 68 insertions(+), 93 deletions(-) diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index a12c25ff6045..64fde266a3f8 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -219,10 +219,7 @@ async function instrumentRequestStartHttpServerSpan( // This is here for backwards compatibility, we used to set this here before method, url: stripUrlQueryAndFragment(ctx.url.href), - ...httpHeadersToSpanAttributes( - winterCGHeadersToDict(request.headers), - getClient()?.getOptions().sendDefaultPii ?? false, - ), + ...httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers)), }; if (parametrizedRoute) { diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 4a079f488474..73998e529349 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -3,7 +3,6 @@ import { captureException, continueTrace, defineIntegration, - getClient, httpHeadersToSpanAttributes, isURLObjectRelative, parseStringToURLObject, @@ -207,9 +206,7 @@ function wrapRequestHandler( routeName = route; } - const client = getClient(); - const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false; - Object.assign(attributes, httpHeadersToSpanAttributes(request.headers.toJSON(), sendDefaultPii)); + Object.assign(attributes, httpHeadersToSpanAttributes(request.headers.toJSON())); isolationScope.setSDKProcessingMetadata({ normalizedRequest: { diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 5c97562d9fde..7908d3dcf48e 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -66,8 +66,7 @@ export function wrapRequestHandler( attributes['user_agent.original'] = userAgentHeader; } - const sendDefaultPii = options.sendDefaultPii ?? false; - Object.assign(attributes, httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers), sendDefaultPii)); + Object.assign(attributes, httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers))); attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts index ffd60f3e8486..1d3985dd8479 100644 --- a/packages/core/src/utils/request.ts +++ b/packages/core/src/utils/request.ts @@ -129,7 +129,19 @@ function getAbsoluteUrl({ } // "-user" because otherwise it would match "user-agent" -const SENSITIVE_HEADER_SNIPPETS = ['auth', 'token', 'secret', 'cookie', '-user', 'password', 'key']; +const SENSITIVE_HEADER_SNIPPETS = [ + 'auth', + 'token', + 'secret', + 'cookie', + '-user', + 'password', + 'key', + 'jwt', + 'bearer', + 'sso', + 'saml', +]; /** * Converts incoming HTTP request headers to OpenTelemetry span attributes following semantic conventions. @@ -140,26 +152,25 @@ const SENSITIVE_HEADER_SNIPPETS = ['auth', 'token', 'secret', 'cookie', '-user', */ export function httpHeadersToSpanAttributes( headers: Record, - sendDefaultPii: boolean = false, ): Record { const spanAttributes: Record = {}; try { Object.entries(headers).forEach(([key, value]) => { - if (value !== undefined) { - const lowerCasedKey = key.toLowerCase(); - - if (!sendDefaultPii && SENSITIVE_HEADER_SNIPPETS.some(snippet => lowerCasedKey.includes(snippet))) { - return; - } + if (value == null) { + return; + } - const normalizedKey = `http.request.header.${lowerCasedKey.replace(/-/g, '_')}`; + const lowerCasedKey = key.toLowerCase(); + const isSensitive = SENSITIVE_HEADER_SNIPPETS.some(snippet => lowerCasedKey.includes(snippet)); + const normalizedKey = `http.request.header.${lowerCasedKey.replace(/-/g, '_')}`; - if (Array.isArray(value)) { - spanAttributes[normalizedKey] = value.map(v => (v !== null && v !== undefined ? String(v) : v)).join(';'); - } else if (typeof value === 'string') { - spanAttributes[normalizedKey] = value; - } + if (isSensitive) { + spanAttributes[normalizedKey] = '[Filtered]'; + } else if (Array.isArray(value)) { + spanAttributes[normalizedKey] = value.map(v => (v != null ? String(v) : v)).join(';'); + } else if (typeof value === 'string') { + spanAttributes[normalizedKey] = value; } }); } catch { diff --git a/packages/core/test/lib/utils/request.test.ts b/packages/core/test/lib/utils/request.test.ts index b37ee860f43f..328aebf29209 100644 --- a/packages/core/test/lib/utils/request.test.ts +++ b/packages/core/test/lib/utils/request.test.ts @@ -613,61 +613,25 @@ describe('request utils', () => { }); describe('PII filtering', () => { - it('filters out sensitive headers when sendDefaultPii is false (default)', () => { - const headers = { - 'Content-Type': 'application/json', - 'User-Agent': 'test-agent', - Authorization: 'Bearer secret-token', - Cookie: 'session=abc123', - 'X-API-Key': 'api-key-123', - 'X-Auth-Token': 'auth-token-456', - }; - - const result = httpHeadersToSpanAttributes(headers, false); - - expect(result).toEqual({ - 'http.request.header.content_type': 'application/json', - 'http.request.header.user_agent': 'test-agent', - // Sensitive headers should be filtered out - }); - }); - - it('includes sensitive headers when sendDefaultPii is true', () => { - const headers = { - 'Content-Type': 'application/json', - 'User-Agent': 'test-agent', - Authorization: 'Bearer secret-token', - Cookie: 'session=abc123', - 'X-API-Key': 'api-key-123', - }; - - const result = httpHeadersToSpanAttributes(headers, true); - - expect(result).toEqual({ - 'http.request.header.content_type': 'application/json', - 'http.request.header.user_agent': 'test-agent', - 'http.request.header.authorization': 'Bearer secret-token', - 'http.request.header.cookie': 'session=abc123', - 'http.request.header.x_api_key': 'api-key-123', - }); - }); - it('filters sensitive headers case-insensitively', () => { const headers = { AUTHORIZATION: 'Bearer secret-token', Cookie: 'session=abc123', - 'x-api-key': 'key-123', + 'x-aPi-kEy': 'key-123', 'Content-Type': 'application/json', }; - const result = httpHeadersToSpanAttributes(headers, false); + const result = httpHeadersToSpanAttributes(headers); expect(result).toEqual({ 'http.request.header.content_type': 'application/json', + 'http.request.header.cookie': '[Filtered]', + 'http.request.header.x_api_key': '[Filtered]', + 'http.request.header.authorization': '[Filtered]', }); }); - it('filters comprehensive list of sensitive headers', () => { + it('always filters comprehensive list of sensitive headers', () => { const headers = { 'Content-Type': 'application/json', 'User-Agent': 'test-agent', @@ -692,15 +656,41 @@ describe('request utils', () => { 'X-Private-Key': 'private', 'X-Forwarded-user': 'user', 'X-Forwarded-authorization': 'auth', + 'x-jwt-token': 'jwt', + 'x-bearer-token': 'bearer', + 'x-sso-token': 'sso', + 'x-saml-token': 'saml', }; - const result = httpHeadersToSpanAttributes(headers, false); + const result = httpHeadersToSpanAttributes(headers); + // Sensitive headers are always included and redacted expect(result).toEqual({ 'http.request.header.content_type': 'application/json', 'http.request.header.user_agent': 'test-agent', 'http.request.header.accept': 'application/json', 'http.request.header.host': 'example.com', + 'http.request.header.authorization': '[Filtered]', + 'http.request.header.cookie': '[Filtered]', + 'http.request.header.set_cookie': '[Filtered]', + 'http.request.header.x_api_key': '[Filtered]', + 'http.request.header.x_auth_token': '[Filtered]', + 'http.request.header.x_secret': '[Filtered]', + 'http.request.header.x_secret_key': '[Filtered]', + 'http.request.header.www_authenticate': '[Filtered]', + 'http.request.header.proxy_authorization': '[Filtered]', + 'http.request.header.x_access_token': '[Filtered]', + 'http.request.header.x_csrf_token': '[Filtered]', + 'http.request.header.x_xsrf_token': '[Filtered]', + 'http.request.header.x_session_token': '[Filtered]', + 'http.request.header.x_password': '[Filtered]', + 'http.request.header.x_private_key': '[Filtered]', + 'http.request.header.x_forwarded_user': '[Filtered]', + 'http.request.header.x_forwarded_authorization': '[Filtered]', + 'http.request.header.x_jwt_token': '[Filtered]', + 'http.request.header.x_bearer_token': '[Filtered]', + 'http.request.header.x_sso_token': '[Filtered]', + 'http.request.header.x_saml_token': '[Filtered]', }); }); }); diff --git a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts index 4e8cdb3fe7c9..ff025fc3ecc7 100644 --- a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts +++ b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts @@ -1,5 +1,5 @@ import type { Span, WebFetchHeaders } from '@sentry/core'; -import { getClient, httpHeadersToSpanAttributes, winterCGHeadersToDict } from '@sentry/core'; +import { httpHeadersToSpanAttributes, winterCGHeadersToDict } from '@sentry/core'; /** * Extracts HTTP request headers as span attributes and optionally applies them to a span. @@ -12,15 +12,12 @@ export function addHeadersAsAttributes( return {}; } - const client = getClient(); - const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false; - const headersDict: Record = headers instanceof Headers || (typeof headers === 'object' && 'get' in headers) ? winterCGHeadersToDict(headers as Headers) : headers; - const headerAttributes = httpHeadersToSpanAttributes(headersDict, sendDefaultPii); + const headerAttributes = httpHeadersToSpanAttributes(headersDict); if (span) { span.setAttributes(headerAttributes); diff --git a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts index c24c0c68d1da..34741e95c912 100644 --- a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts @@ -136,7 +136,6 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions const method = normalizedRequest.method || request.method?.toUpperCase() || 'GET'; const httpTargetWithoutQueryFragment = urlObj ? urlObj.pathname : stripUrlQueryAndFragment(fullUrl); const bestEffortTransactionName = `${method} ${httpTargetWithoutQueryFragment}`; - const shouldSendDefaultPii = client.getOptions().sendDefaultPii ?? false; // We use the plain tracer.startSpan here so we can pass the span kind const span = tracer.startSpan(bestEffortTransactionName, { @@ -158,7 +157,7 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions 'http.flavor': httpVersion, 'net.transport': httpVersion?.toUpperCase() === 'QUIC' ? 'ip_udp' : 'ip_tcp', ...getRequestContentLengthAttribute(request), - ...httpHeadersToSpanAttributes(normalizedRequest.headers || {}, shouldSendDefaultPii), + ...httpHeadersToSpanAttributes(normalizedRequest.headers || {}), }, }); diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index a04b866cd774..4b41d6e8ab82 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -3,7 +3,6 @@ import { captureException, debug, flushIfServerless, - getClient, httpHeadersToSpanAttributes, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -171,13 +170,9 @@ function getSpanAttributes( attributes['http.route'] = event.path; } - // Extract and add HTTP headers as span attributes - const client = getClient(); - const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false; - // Get headers from the Node.js request object const headers = event.node?.req?.headers || {}; - const headerAttributes = httpHeadersToSpanAttributes(headers, sendDefaultPii); + const headerAttributes = httpHeadersToSpanAttributes(headers); // Merge header attributes with existing attributes Object.assign(attributes, headerAttributes); diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts index fda9b3f10b75..2416699cb2a6 100644 --- a/packages/remix/src/server/instrumentServer.ts +++ b/packages/remix/src/server/instrumentServer.ts @@ -310,10 +310,7 @@ function wrapRequestHandler ServerBuild | Promise [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', method: request.method, - ...httpHeadersToSpanAttributes( - winterCGHeadersToDict(request.headers), - clientOptions.sendDefaultPii ?? false, - ), + ...httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers)), }, }, async span => { diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index 26872a0f6f24..3d9963bd1056 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -3,7 +3,6 @@ import { continueTrace, debug, flushIfServerless, - getClient, getCurrentScope, getDefaultIsolationScope, getIsolationScope, @@ -179,10 +178,7 @@ async function instrumentHandle( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeName ? 'route' : 'url', 'sveltekit.tracing.original_name': originalName, - ...httpHeadersToSpanAttributes( - winterCGHeadersToDict(event.request.headers), - getClient()?.getOptions().sendDefaultPii ?? false, - ), + ...httpHeadersToSpanAttributes(winterCGHeadersToDict(event.request.headers)), }); } @@ -208,10 +204,7 @@ async function instrumentHandle( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url', 'http.method': event.request.method, - ...httpHeadersToSpanAttributes( - winterCGHeadersToDict(event.request.headers), - getClient()?.getOptions().sendDefaultPii ?? false, - ), + ...httpHeadersToSpanAttributes(winterCGHeadersToDict(event.request.headers)), }, name: routeName, }, From 02aa2ea072fa956c805eeb6f463fb6ed763efa57 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:45:17 +0100 Subject: [PATCH 32/32] meta(changelog): Update changelog for 10.27.0 --- CHANGELOG.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bee97ac9189..58e2cf7bd830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,11 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @bignoncedric and @adam-kov. Thank you for your contributions! +## 10.27.0 + +### Important Changes -- feat(deps): Bump OpenTelemetry ([#18239](https://github.com/getsentry/sentry-javascript/pull/18239)) +- **feat(deps): Bump OpenTelemetry ([#18239](https://github.com/getsentry/sentry-javascript/pull/18239))** - Bump @opentelemetry/context-async-hooks from ^2.1.0 to ^2.2.0 - Bump @opentelemetry/core from ^2.1.0 to ^2.2.0 - Bump @opentelemetry/resources from ^2.1.0 to ^2.2.0 @@ -39,6 +41,49 @@ Work in this release was contributed by @bignoncedric and @adam-kov. Thank you f - Bump @opentelemetry/instrumentation-undici from 0.15.0 to 0.19.0 - Bump @prisma/instrumentation from 6.15.0 to 6.19.0 +- **feat(browserprofiling): Add `manual` mode and deprecate old profiling ([#18189](https://github.com/getsentry/sentry-javascript/pull/18189))** + + Adds the `manual` lifecycle mode for UI profiling (the default mode), allowing profiles to be captured manually with `Sentry.uiProfiler.startProfiler()` and `Sentry.uiProfiler.stopProfiler()`. + The previous transaction-based profiling is with `profilesSampleRate` is now deprecated in favor of the new UI Profiling with `profileSessionSampleRate`. + +### Other Changes + +- feat(core): Add `gibibyte` and `pebibyte` to `InformationUnit` type ([#18241](https://github.com/getsentry/sentry-javascript/pull/18241)) +- feat(core): Add scope attribute APIs ([#18165](https://github.com/getsentry/sentry-javascript/pull/18165)) +- feat(core): Re-add `_experiments.enableLogs` option ([#18299](https://github.com/getsentry/sentry-javascript/pull/18299)) +- feat(core): Use `maxValueLength` on error messages ([#18301](https://github.com/getsentry/sentry-javascript/pull/18301)) +- feat(deps): bump @sentry/bundler-plugin-core from 4.3.0 to 4.6.1 ([#18273](https://github.com/getsentry/sentry-javascript/pull/18273)) +- feat(deps): bump @sentry/cli from 2.56.0 to 2.58.2 ([#18271](https://github.com/getsentry/sentry-javascript/pull/18271)) +- feat(node): Add tracing support for AzureOpenAI ([#18281](https://github.com/getsentry/sentry-javascript/pull/18281)) +- feat(node): Fix local variables capturing for out-of-app frames ([#18245](https://github.com/getsentry/sentry-javascript/pull/18245)) +- fix(core): Add a PromiseBuffer for incoming events on the client ([#18120](https://github.com/getsentry/sentry-javascript/pull/18120)) +- fix(core): Always redact content of sensitive headers regardless of `sendDefaultPii` ([#18311](https://github.com/getsentry/sentry-javascript/pull/18311)) +- fix(metrics): Update return type of `beforeSendMetric` ([#18261](https://github.com/getsentry/sentry-javascript/pull/18261)) +- fix(nextjs): universal random tunnel path support ([#18257](https://github.com/getsentry/sentry-javascript/pull/18257)) +- ref(react): Add more guarding against wildcards in lazy route transactions ([#18155](https://github.com/getsentry/sentry-javascript/pull/18155)) +- chore(deps): bump glob from 11.0.1 to 11.1.0 in /packages/react-router ([#18243](https://github.com/getsentry/sentry-javascript/pull/18243)) + +
+ Internal Changes + - build(deps): bump hono from 4.9.7 to 4.10.3 in /dev-packages/e2e-tests/test-applications/cloudflare-hono ([#18038](https://github.com/getsentry/sentry-javascript/pull/18038)) + - chore: Add `bump_otel_instrumentations` cursor command ([#18253](https://github.com/getsentry/sentry-javascript/pull/18253)) + - chore: Add external contributor to CHANGELOG.md ([#18297](https://github.com/getsentry/sentry-javascript/pull/18297)) + - chore: Add external contributor to CHANGELOG.md ([#18300](https://github.com/getsentry/sentry-javascript/pull/18300)) + - chore: Do not update opentelemetry ([#18254](https://github.com/getsentry/sentry-javascript/pull/18254)) + - chore(angular): Add Angular 21 Support ([#18274](https://github.com/getsentry/sentry-javascript/pull/18274)) + - chore(deps): bump astro from 4.16.18 to 5.15.9 in /dev-packages/e2e-tests/test-applications/cloudflare-astro ([#18259](https://github.com/getsentry/sentry-javascript/pull/18259)) + - chore(dev-deps): Update some dev dependencies ([#17816](https://github.com/getsentry/sentry-javascript/pull/17816)) + - ci(deps): Bump actions/create-github-app-token from 2.1.1 to 2.1.4 ([#17825](https://github.com/getsentry/sentry-javascript/pull/17825)) + - ci(deps): bump actions/setup-node from 4 to 6 ([#18077](https://github.com/getsentry/sentry-javascript/pull/18077)) + - ci(deps): bump actions/upload-artifact from 4 to 5 ([#18075](https://github.com/getsentry/sentry-javascript/pull/18075)) + - ci(deps): bump github/codeql-action from 3 to 4 ([#18076](https://github.com/getsentry/sentry-javascript/pull/18076)) + - doc(sveltekit): Update documentation link for SvelteKit guide ([#18298](https://github.com/getsentry/sentry-javascript/pull/18298)) + - test(e2e): Fix astro config in test app ([#18282](https://github.com/getsentry/sentry-javascript/pull/18282)) + - test(nextjs): Remove debug logs from e2e test ([#18250](https://github.com/getsentry/sentry-javascript/pull/18250)) +
+ +Work in this release was contributed by @bignoncedric and @adam-kov. Thank you for your contributions! + ## 10.26.0 ### Important Changes