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. 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 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/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 0507fe879c27..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 }} @@ -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 46d6e7d4fac9..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' @@ -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 }} @@ -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 @@ -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 @@ -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 @@ -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: @@ -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 @@ -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}} @@ -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 @@ -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 }}) @@ -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 @@ -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/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/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 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 5103f1f43a2d..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' @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 05c465036ce4..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 }} @@ -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 diff --git a/.size-limit.js b/.size-limit.js index 184aad0698f4..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.38 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.33 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.2 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 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 KB', + limit: '52 KB', }, // Node SDK (ESM) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eba860932aa..58e2cf7bd830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,86 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 10.27.0 + +### Important Changes + +- **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 + +- **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 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/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/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 000000000000..57614f9c9675 Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/angular-21/public/favicon.ico differ 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/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 5adbcd6ad75f..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,9 +17,9 @@ "test:assert": "pnpm -v" }, "dependencies": { - "@astrojs/cloudflare": "8.1.0", + "@astrojs/cloudflare": "12.6.11", "@sentry/astro": "latest || *", - "astro": "4.16.18" + "astro": "5.15.9" }, "devDependencies": { "@astrojs/internal-helpers": "0.4.1" 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", 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; 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 000000000000..718d6fea4835 Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/favicon.ico differ 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/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/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/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/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/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/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/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": { 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/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/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/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/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/client.ts b/packages/core/src/client.ts index 1c925d930036..805d8e596528 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); @@ -234,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); @@ -275,9 +285,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 +312,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 +347,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; @@ -801,6 +818,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. */ @@ -1017,6 +1052,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()`. */ @@ -1252,7 +1297,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 +1380,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 +1459,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/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/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/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; 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'; diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index ccdc3b180e15..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; }; /** @@ -420,9 +428,9 @@ export interface ClientOptions Metric; + beforeSendMetric?: (metric: Metric) => Metric | null; /** * Function to compute tracing sample rate dynamically and filter unwanted traces. 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/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/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/client.test.ts b/packages/core/test/lib/client.test.ts index c009d0e0c2a8..a59a8bbb8780 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -15,15 +15,15 @@ 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'; 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, TestIntegration } from '../mocks/integration'; +import { AdHocIntegration, AsyncTestIntegration, TestIntegration } from '../mocks/integration'; import { makeFakeTransport } from '../mocks/transport'; import { clearGlobalScope } from '../testutils'; @@ -36,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', () => { @@ -262,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); @@ -2705,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(); @@ -2935,4 +2994,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/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' }); 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/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'; 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/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/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/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') { 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-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/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); } 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/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(); +// 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 '*' + }); + }); }); diff --git a/packages/remix/package.json b/packages/remix/package.json index 667f77d53648..98bdd9a39c7c 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -65,10 +65,10 @@ }, "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.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/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/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/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 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/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, }, 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 082e1a032283..41c2c2fe1486 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" @@ -5954,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" @@ -5973,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/core" "2.1.0" + "@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.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" @@ -6451,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" @@ -7084,7 +7065,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== @@ -7098,50 +7084,64 @@ 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/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" + 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.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== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -7149,14 +7149,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" @@ -8863,14 +8863,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" @@ -8931,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 "*" @@ -9081,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" @@ -10507,12 +10494,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 +10512,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 +11191,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 +12521,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 +12529,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 +13684,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 +14422,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 +15945,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 +16062,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,24 +17310,19 @@ 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: +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== @@ -17915,14 +17897,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" @@ -17950,10 +17932,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" @@ -18911,9 +18893,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" @@ -19102,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" @@ -19460,7 +19442,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 +19791,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" @@ -20035,7 +20013,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== @@ -20282,18 +20260,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: @@ -22301,10 +22279,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" @@ -24606,7 +24584,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 +24596,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" @@ -26729,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" @@ -27252,7 +27234,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== @@ -27475,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== @@ -27593,7 +27575,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 +27623,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" @@ -27760,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" @@ -28842,6 +28820,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" @@ -29043,9 +29022,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 +29032,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 +29405,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 +29424,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 +29815,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 +30004,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 +31481,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: